![]() 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
/**
* Artist Profile - Clean Implementation
* This file contains the optimized artist profile code
* It can be called directly or included from artist_profile.php
*/
// Include translation system
require_once 'includes/translations.php';
// Include audio token system for signed URLs
require_once 'utils/audio_token.php';
// Helper function to format profile image
if (!function_exists('formatProfileImage')) {
function formatProfileImage($profile_image, $name) {
// If no profile image, return null (will use initials)
if (empty($profile_image)) {
return null;
}
// Trim whitespace
$profile_image = trim($profile_image);
// If empty after trim, return null
if (empty($profile_image) || $profile_image === 'null' || $profile_image === 'NULL') {
return null;
}
// If it's already a full URL (http/https), return as is
if (preg_match('/^https?:\/\//i', $profile_image)) {
return $profile_image;
}
// If it starts with /, it's a valid relative path
if (strpos($profile_image, '/') === 0) {
return $profile_image;
}
// If it looks like a file path but doesn't start with /, try to fix it
// Check if it contains common image extensions
if (preg_match('/\.(jpg|jpeg|png|gif|webp)$/i', $profile_image)) {
// If it's in uploads directory, add leading slash
if (strpos($profile_image, 'uploads/') !== false || strpos($profile_image, 'profile') !== false) {
// Check if it already has a directory structure
if (strpos($profile_image, '/') === false) {
// It's just a filename, assume it's in uploads/profile_images/
return '/uploads/profile_images/' . $profile_image;
} else {
// It has a path but no leading slash
return '/' . ltrim($profile_image, '/');
}
}
// If it starts with a directory name (like 'ds/'), try to construct proper path
if (preg_match('/^[a-z0-9_]+\//i', $profile_image)) {
// This might be a malformed path, try to find it in uploads
$filename = basename($profile_image);
return '/uploads/profile_images/' . $filename;
}
}
// If we can't determine a valid image path, return null (will use initials)
return null;
}
}
if (!function_exists('ssp_format_ordinal')) {
/**
* Format numbers with ordinal suffix (1st, 2nd, 3rd, etc.)
*/
function ssp_format_ordinal($number) {
$n = (int)$number;
if ($n <= 0) {
return $n;
}
$lang = function_exists('getCurrentLanguage') ? getCurrentLanguage() : 'en';
if ($lang === 'fr') {
return $n . 'e';
}
$mod100 = $n % 100;
if ($mod100 >= 11 && $mod100 <= 13) {
return $n . 'th';
}
switch ($n % 10) {
case 1:
return $n . 'st';
case 2:
return $n . 'nd';
case 3:
return $n . 'rd';
default:
return $n . 'th';
}
}
}
if (!function_exists('ssp_format_event_date_range')) {
function ssp_format_event_date_range($startDate, $endDate = null) {
if (empty($startDate)) {
return '';
}
try {
$start = new DateTime($startDate);
if (!empty($endDate)) {
$end = new DateTime($endDate);
if ($start->format('Y-m-d') === $end->format('Y-m-d')) {
return $start->format('M j, Y g:i A') . ' - ' . $end->format('g:i A');
}
return $start->format('M j, Y g:i A') . ' â ' . $end->format('M j, Y g:i A');
}
return $start->format('M j, Y g:i A');
} catch (Exception $e) {
return $startDate;
}
}
}
// Check if we're being called from the main file
if (!defined('CALLED_FROM_MAIN')) {
// If called directly, configure session and database
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 0); // Set to 1 if using HTTPS
ini_set('session.cookie_samesite', 'Lax');
ini_set('session.use_strict_mode', 1);
ini_set('session.use_cookies', 1);
ini_set('session.use_only_cookies', 1);
session_start();
// 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');
// Ensure session cookie is set
if (!isset($_COOKIE[session_name()])) {
setcookie(session_name(), session_id(), time() + 3600, '/', '', false, true);
}
// Handle AJAX requests
$is_ajax = isset($_GET['ajax']) && $_GET['ajax'] == '1';
// Database logic and redirects must come BEFORE any output
require_once 'config/database.php';
$pdo = getDBConnection();
} else {
// If called from main file, we need to get the database connection and session
if (!isset($pdo)) {
require_once 'config/database.php';
$pdo = getDBConnection();
}
if (!isset($is_ajax)) {
$is_ajax = isset($_GET['ajax']) && $_GET['ajax'] == '1';
}
// CRITICAL: Ensure session is available when included
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
}
// Get artist ID from URL or custom URL
$artist_id = 0;
$custom_url = isset($_GET['custom_url']) ? strtolower(trim($_GET['custom_url'])) : null;
if (isset($_GET['id'])) {
// SECURITY: Validate that artist_id is a positive integer
$artist_id_raw = $_GET['id'];
if (!is_numeric($artist_id_raw) || (int)$artist_id_raw <= 0) {
error_log("SECURITY: Invalid artist_id attempt in artist_profile_clean.php: " . htmlspecialchars($artist_id_raw, ENT_QUOTES, 'UTF-8'));
header('Location: /artists.php');
exit;
}
$artist_id = (int)$artist_id_raw;
} elseif ($custom_url) {
// Look up user by custom URL (case-insensitive)
$stmt = $pdo->prepare("SELECT user_id FROM user_profiles WHERE LOWER(custom_url) = ?");
$stmt->execute([$custom_url]);
$result = $stmt->fetch();
if ($result) {
$artist_id = $result['user_id'];
}
}
if (!$artist_id) {
header('Location: /artists.php');
exit;
}
// Get artist data with comprehensive stats
$stmt = $pdo->prepare("
SELECT
u.id,
u.name as username,
u.email,
u.plan,
u.credits,
u.created_at as joined_date,
COALESCE(NULLIF(up.profile_image, ''), NULLIF(u.profile_image, ''), NULL) as profile_image,
up.cover_image,
up.cover_position,
up.profile_position,
up.custom_url,
up.bio,
up.location,
up.website,
up.social_links,
up.genres,
up.music_style,
up.artist_highlights,
up.influences,
up.equipment,
up.achievements,
up.featured_tracks,
up.artist_statement,
up.paypal_me_username,
COUNT(CASE WHEN mt.is_public = 1 THEN 1 END) as total_tracks,
COUNT(CASE WHEN mt.status = 'complete' AND mt.is_public = 1 THEN 1 END) as completed_tracks,
COUNT(CASE WHEN mt.status = 'processing' THEN 1 END) as processing_tracks,
COUNT(CASE WHEN mt.status = 'failed' THEN 1 END) as failed_tracks,
MAX(CASE WHEN mt.is_public = 1 THEN mt.created_at END) as last_activity,
COALESCE(SUM(CASE WHEN mt.status = 'complete' AND mt.is_public = 1 THEN mt.duration ELSE 0 END), 0) as total_duration,
COALESCE((
SELECT COUNT(*)
FROM track_plays tp
JOIN music_tracks mt ON tp.track_id = mt.id
WHERE mt.user_id = u.id AND mt.status = 'complete' AND mt.is_public = 1
), 0) as total_plays,
COALESCE((
SELECT COUNT(*)
FROM track_likes tl
JOIN music_tracks mt ON tl.track_id = mt.id
WHERE mt.user_id = u.id AND mt.status = 'complete' AND mt.is_public = 1
), 0) as total_likes,
(SELECT COUNT(*) FROM user_follows WHERE following_id = u.id) as followers_count,
(SELECT COUNT(*) FROM user_follows WHERE follower_id = u.id) as following_count,
(SELECT COUNT(*) FROM user_friends WHERE status = 'accepted' AND (user_id = u.id OR friend_id = u.id)) as friends_count,
(SELECT COUNT(*) FROM artist_playlists WHERE user_id = u.id) as playlists_count,
CASE
WHEN COUNT(CASE WHEN mt.status = 'complete' AND mt.is_public = 1 THEN 1 END) >= 20 THEN 'đĩ Pro Creator'
WHEN COUNT(CASE WHEN mt.status = 'complete' AND mt.is_public = 1 THEN 1 END) >= 10 THEN 'â Rising Star'
WHEN COUNT(CASE WHEN mt.status = 'complete' AND mt.is_public = 1 THEN 1 END) >= 5 THEN 'đĨ Active Artist'
WHEN COUNT(CASE WHEN mt.status = 'complete' AND mt.is_public = 1 THEN 1 END) >= 1 THEN 'đŧ New Artist'
ELSE 'đ¤ Member'
END as badge
FROM users u
LEFT JOIN user_profiles up ON u.id = up.user_id
LEFT JOIN music_tracks mt ON u.id = mt.user_id
WHERE u.id = ?
GROUP BY u.id, u.name, u.email, u.plan, u.credits, u.created_at, up.profile_image, u.profile_image, up.cover_image, up.cover_position, up.profile_position, up.custom_url, up.bio, up.location, up.website, up.social_links, up.genres, up.music_style, up.artist_highlights, up.influences, up.equipment, up.achievements, up.featured_tracks, up.artist_statement, up.paypal_me_username
");
$stmt->execute([$artist_id]);
$artist = $stmt->fetch();
if (!$artist) {
header('Location: /artists.php');
exit;
}
// IMPORTANT: Allow bots/crawlers (like Facebook scraper) to access artist profiles even without session
// This is critical for Facebook, Twitter, etc. to scrape the page and show previews
$isBot = isset($_SERVER['HTTP_USER_AGENT']) && (
stripos($_SERVER['HTTP_USER_AGENT'], 'facebookexternalhit') !== false ||
stripos($_SERVER['HTTP_USER_AGENT'], 'Twitterbot') !== false ||
stripos($_SERVER['HTTP_USER_AGENT'], 'LinkedInBot') !== false ||
stripos($_SERVER['HTTP_USER_AGENT'], 'WhatsApp') !== false ||
stripos($_SERVER['HTTP_USER_AGENT'], 'bot') !== false ||
stripos($_SERVER['HTTP_USER_AGENT'], 'crawler') !== false ||
stripos($_SERVER['HTTP_USER_AGENT'], 'spider') !== false
);
// Get artist's tracks with preferred variations
// IMPORTANT: Use INNER JOIN to ensure only tracks that actually exist are shown
// Use GROUP BY to prevent duplicates from LEFT JOIN with user_variation_preferences
// Ensure wishlist table exists before referencing it in queries
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 table ensure error: ' . $e->getMessage());
}
// 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;
}
// OPTIMIZED: Using JOINs instead of correlated subqueries for better performance
$vote_count_select = $votes_table_exists ?
"COALESCE(vote_stats.vote_count, 0) as vote_count," :
"0 as vote_count,";
$user_vote_select = $votes_table_exists ?
"user_vote_stats.vote_type as user_vote," :
"NULL as user_vote,";
$vote_joins = $votes_table_exists ?
"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, vote_type FROM track_votes WHERE user_id = ?) user_vote_stats ON mt.id = user_vote_stats.track_id" :
"";
$stmt = $pdo->prepare("
SELECT
mt.id,
mt.title,
mt.prompt,
mt.audio_url,
mt.duration,
mt.price,
mt.image_url,
mt.metadata,
mt.status,
mt.is_public,
mt.user_id,
mt.created_at,
mt.task_id,
mt.variations_count,
u.name as artist_name,
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,
COALESCE(purchase_stats.purchase_count, 0) as purchase_count,
COALESCE(rating_stats.average_rating, 0) as average_rating,
COALESCE(rating_stats.rating_count, 0) as rating_count,
" . $vote_count_select . "
" . $user_vote_select . "
CASE WHEN user_like_stats.track_id IS NOT NULL THEN 1 ELSE 0 END as user_liked,
CASE WHEN wishlist_stats.track_id IS NOT NULL THEN 1 ELSE 0 END as is_in_wishlist,
CASE WHEN purchase_user_stats.track_id IS NOT NULL THEN 1 ELSE 0 END as user_purchased,
MAX(uvp.variation_id) as preferred_variation_id
FROM music_tracks mt
INNER JOIN users u ON mt.user_id = u.id
LEFT JOIN user_variation_preferences uvp ON mt.id = uvp.track_id AND uvp.user_id = ? AND uvp.is_main_track = TRUE
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, COUNT(*) as purchase_count FROM track_purchases GROUP BY track_id) purchase_stats ON mt.id = purchase_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 FROM track_purchases WHERE user_id = ?) purchase_user_stats ON mt.id = purchase_user_stats.track_id
" . $vote_joins . "
WHERE mt.user_id = ?
AND mt.status = 'complete'
AND mt.is_public = 1
AND mt.id IS NOT NULL
AND u.id IS NOT NULL
GROUP BY mt.id, mt.title, mt.prompt, mt.audio_url, mt.duration, mt.price, mt.image_url, mt.metadata, mt.status, mt.is_public, mt.user_id, mt.created_at, mt.task_id, mt.variations_count, u.name, like_stats.like_count, comment_stats.comment_count, play_stats.play_count, share_stats.share_count, purchase_stats.purchase_count, rating_stats.average_rating, rating_stats.rating_count, user_like_stats.track_id, wishlist_stats.track_id, purchase_user_stats.track_id" . ($votes_table_exists ? ", vote_stats.vote_count, user_vote_stats.vote_type" : "") . "
ORDER BY mt.created_at DESC
LIMIT 20
");
$current_user_id = $_SESSION['user_id'] ?? 0;
// Execute with user_id parameters for JOINs (if votes table exists, add one more user_id parameter)
// Parameters: uvp.user_id, user_like_stats.user_id, wishlist_stats.user_id, purchase_user_stats.user_id, [user_vote_stats.user_id if votes exist], artist_id
$params = $votes_table_exists
? [$current_user_id, $current_user_id, $current_user_id, $current_user_id, $current_user_id, $artist_id]
: [$current_user_id, $current_user_id, $current_user_id, $current_user_id, $artist_id];
$stmt->execute($params);
$tracks = $stmt->fetchAll();
// Remove duplicate tracks by ID (in case LEFT JOIN with user_variation_preferences causes duplicates)
$seen_track_ids = [];
$unique_tracks = [];
foreach ($tracks as $track) {
if (!in_array($track['id'], $seen_track_ids)) {
$seen_track_ids[] = $track['id'];
$unique_tracks[] = $track;
}
}
$tracks = $unique_tracks;
// 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);
}
}
// IMPORTANT: Always use the main track audio_url, NOT variations
// Variations might be preview files (44 seconds), we want the full track
// This matches the track.php behavior which uses $track['audio_url'] by default
foreach ($tracks as &$track) {
// Always use the main track's audio_url to ensure full playback
// Don't use preferred_audio_url which might point to a preview variation
$track['playback_audio_url'] = $track['audio_url'];
$track['playback_duration'] = $track['duration'];
}
unset($track); // Unset the reference to prevent issues with the last element
// Get variations for each track
foreach ($tracks as &$track) {
$track['variations'] = [];
if (($track['variations_count'] ?? 0) > 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 a given track
* Returns array with overall rank, plays rank, likes rank, rating rank, total_tracks, and overall_percentile
*/
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);
// Audit log: Track ranking calculated
error_log("đ RANKING CALC: Track ID {$track['id']} ('{$track['title']}') - Rank #{$rankings['overall']}, Score: " . round($total_score, 2) . " (Plays: {$track['play_count']}, Likes: {$track['like_count']}, Votes: " . ($track['vote_count'] ?? 0) . ", Rating: " . round($track['average_rating'] ?? 0, 1) . "x{$track['rating_count']})");
// 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
// Calculate detailed score breakdown for display - matches track.php exactly
$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 artist rankings based on total plays, likes, and ratings
* Similar to track rankings but for artists
*/
function calculateArtistRankings($pdo, $artist) {
$rankings = [];
// Get total artists for context (must match ranking query - only artists with complete AND public tracks)
// Count all artists who have at least one complete, public track
// This must match the ranking query exactly to ensure consistency
// Note: We include artists with complete tracks even if audio_url is missing
// because they may have variations or the audio might be processing
// Count all artists who have at least one complete, public track
// IMPORTANT: This must match the ranking queries and get_all_artist_rankings.php exactly
// We count ALL artists with complete+public tracks, regardless of audio_url
// This ensures consistency across all ranking displays
$total_artists_query = "
SELECT COUNT(DISTINCT u.id)
FROM users u
WHERE EXISTS (
SELECT 1 FROM music_tracks mt
WHERE mt.user_id = u.id
AND mt.status = 'complete'
AND mt.is_public = 1
)
";
$stmt_total = $pdo->query($total_artists_query);
$total_artists = $stmt_total->fetchColumn();
// Debug logging to verify count
error_log("Artist Rankings Debug: Total artists = " . $total_artists . " (artist_id: " . ($artist['id'] ?? 'unknown') . ")");
// OPTIMIZED: Using JOINs instead of multiple subqueries for better performance
// Recalculate artist stats directly from database to ensure consistency
// Don't rely on $artist array values which might be from old query
$artist_id = $artist['id'];
$artist_stats_query = "
SELECT
COALESCE(play_stats.total_plays, 0) as total_plays,
COALESCE(like_stats.total_likes, 0) as total_likes,
COALESCE(rating_stats.avg_rating, 0) as avg_rating,
COALESCE(rating_stats.rating_count, 0) as rating_count
FROM (SELECT ? as artist_id) a
LEFT JOIN (
SELECT mt.user_id, COUNT(*) as total_plays
FROM track_plays tp
JOIN music_tracks mt ON tp.track_id = mt.id
WHERE mt.status = 'complete' AND mt.is_public = 1
GROUP BY mt.user_id
) play_stats ON a.artist_id = play_stats.user_id
LEFT JOIN (
SELECT mt.user_id, COUNT(*) as total_likes
FROM track_likes tl
JOIN music_tracks mt ON tl.track_id = mt.id
WHERE mt.status = 'complete' AND mt.is_public = 1
GROUP BY mt.user_id
) like_stats ON a.artist_id = like_stats.user_id
LEFT JOIN (
SELECT artist_id, AVG(rating) as avg_rating, COUNT(*) as rating_count
FROM artist_ratings
GROUP BY artist_id
) rating_stats ON a.artist_id = rating_stats.artist_id
";
$stats_stmt = $pdo->prepare($artist_stats_query);
$stats_stmt->execute([$artist_id]);
$artist_stats = $stats_stmt->fetch(PDO::FETCH_ASSOC);
$total_plays = (int)($artist_stats['total_plays'] ?? 0);
$total_likes = (int)($artist_stats['total_likes'] ?? 0);
$avg_rating = (float)($artist_stats['avg_rating'] ?? 0);
$rating_count = (int)($artist_stats['rating_count'] ?? 0);
// Calculate overall ranking based on score: Plays à 1.0 + Likes à 2.0 + (Rating à Rating Count à 5.0)
// IMPORTANT: Tie-breaker must match the ORDER BY clause in get_all_artist_rankings.php (created_at DESC)
// Get artist's created_at for tie-breaking
$artist_created_at = $artist['joined_date'] ?? null;
if (!$artist_created_at) {
// Fallback: fetch created_at if not available
$created_at_stmt = $pdo->prepare("SELECT created_at FROM users WHERE id = ?");
$created_at_stmt->execute([$artist_id]);
$artist_created_at = $created_at_stmt->fetchColumn();
}
$overall_rank_query = "
SELECT COUNT(*) + 1 as rank
FROM (
SELECT
u.id as artist_id,
u.created_at,
COALESCE((
SELECT COUNT(*)
FROM track_plays tp
JOIN music_tracks mt ON tp.track_id = mt.id
WHERE mt.user_id = u.id AND mt.status = 'complete' AND mt.is_public = 1
), 0) as total_plays,
COALESCE((
SELECT COUNT(*)
FROM track_likes tl
JOIN music_tracks mt ON tl.track_id = mt.id
WHERE mt.user_id = u.id AND mt.status = 'complete' AND mt.is_public = 1
), 0) as total_likes,
COALESCE((
SELECT AVG(rating)
FROM artist_ratings
WHERE artist_id = u.id
), 0) as avg_rating,
COALESCE((
SELECT COUNT(*)
FROM artist_ratings
WHERE artist_id = u.id
), 0) as rating_count
FROM users u
WHERE EXISTS (
SELECT 1 FROM music_tracks mt
WHERE mt.user_id = u.id
AND mt.status = 'complete'
AND mt.is_public = 1
)
) artist_stats
WHERE (
(total_plays * 1.0 + total_likes * 2.0 + avg_rating * rating_count * 5.0) >
(? * 1.0 + ? * 2.0 + ? * ? * 5.0)
OR (
(total_plays * 1.0 + total_likes * 2.0 + avg_rating * rating_count * 5.0) =
(? * 1.0 + ? * 2.0 + ? * ? * 5.0)
AND created_at > ?
)
)
";
$stmt_rank = $pdo->prepare($overall_rank_query);
$stmt_rank->execute([
$total_plays, $total_likes, $avg_rating, $rating_count,
$total_plays, $total_likes, $avg_rating, $rating_count, $artist_created_at
]);
$rankings['overall'] = $stmt_rank->fetchColumn();
// Play count ranking
$play_rank_query = "
SELECT COUNT(*) + 1
FROM (
SELECT
u.id,
COALESCE((
SELECT COUNT(*)
FROM track_plays tp
JOIN music_tracks mt ON tp.track_id = mt.id
WHERE mt.user_id = u.id AND mt.status = 'complete' AND mt.is_public = 1
), 0) as total_plays
FROM users u
WHERE EXISTS (
SELECT 1 FROM music_tracks mt
WHERE mt.user_id = u.id
AND mt.status = 'complete'
AND mt.is_public = 1
)
) artist_plays
WHERE total_plays > ?
";
$stmt_rank = $pdo->prepare($play_rank_query);
$stmt_rank->execute([$total_plays]);
$rankings['plays'] = $stmt_rank->fetchColumn();
// Like count ranking
$like_rank_query = "
SELECT COUNT(*) + 1
FROM (
SELECT
u.id,
COALESCE((
SELECT COUNT(*)
FROM track_likes tl
JOIN music_tracks mt ON tl.track_id = mt.id
WHERE mt.user_id = u.id AND mt.status = 'complete' AND mt.is_public = 1
), 0) as total_likes
FROM users u
WHERE EXISTS (
SELECT 1 FROM music_tracks mt
WHERE mt.user_id = u.id
AND mt.status = 'complete'
AND mt.is_public = 1
)
) artist_likes
WHERE total_likes > ?
";
$stmt_rank = $pdo->prepare($like_rank_query);
$stmt_rank->execute([$total_likes]);
$rankings['likes'] = $stmt_rank->fetchColumn();
// Rating ranking (based on average rating with minimum rating count)
if ($rating_count >= 3) {
$rating_rank_query = "
SELECT COUNT(*) + 1
FROM (
SELECT
artist_id,
AVG(rating) as avg_rating,
COUNT(*) as rating_count
FROM artist_ratings
GROUP BY artist_id
HAVING rating_count >= 3
) artist_ratings_ranked
WHERE avg_rating > ?
";
$stmt_rank = $pdo->prepare($rating_rank_query);
$stmt_rank->execute([$avg_rating]);
$rankings['rating'] = $stmt_rank->fetchColumn();
}
$rankings['total_artists'] = $total_artists;
// Percentile: Top X% means you're in the top X% of artists
// Formula: ((total - rank + 1) / total) * 100
// Rank 1 out of 10 = (10-1+1)/10 = 100% (top 100%, meaning best)
// Rank 2 out of 10 = (10-2+1)/10 = 90% (top 90%)
// Rank 10 out of 10 = (10-10+1)/10 = 10% (top 10%)
$rankings['overall_percentile'] = round((($total_artists - $rankings['overall'] + 1) / max($total_artists, 1)) * 100, 1);
// Calculate detailed score breakdown for display
$rankings['score_breakdown'] = [
'plays_score' => $total_plays * 1.0,
'likes_score' => $total_likes * 2.0,
'rating_score' => $avg_rating * $rating_count * 5.0,
'total_score' => ($total_plays * 1.0) + ($total_likes * 2.0) + ($avg_rating * $rating_count * 5.0)
];
return $rankings;
}
// Calculate artist rankings
$artistRankings = calculateArtistRankings($pdo, $artist);
// Get similar artists
$stmt = $pdo->prepare("
SELECT
u.id,
u.name as username,
up.profile_image,
up.profile_position,
COUNT(CASE WHEN mt.status = 'complete' AND mt.is_public = 1 THEN 1 END) as track_count,
(SELECT COUNT(*) FROM user_follows WHERE following_id = u.id) as followers_count,
COALESCE((
SELECT COUNT(*)
FROM track_plays tp
JOIN music_tracks mt2 ON tp.track_id = mt2.id
WHERE mt2.user_id = u.id AND mt2.status = 'complete' AND mt2.is_public = 1
), 0) as total_plays,
CASE
WHEN ? IS NOT NULL AND EXISTS (
SELECT 1 FROM user_follows
WHERE follower_id = ? AND following_id = u.id
) THEN 1
ELSE 0
END as is_following
FROM users u
LEFT JOIN user_profiles up ON u.id = up.user_id
LEFT JOIN music_tracks mt ON u.id = mt.user_id
WHERE u.id != ? AND mt.status = 'complete' AND mt.is_public = 1
GROUP BY u.id, u.name, up.profile_image, up.profile_position
ORDER BY followers_count DESC
LIMIT 6
");
$user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null;
$stmt->execute([$user_id, $user_id, $artist_id]);
$similar_artists = $stmt->fetchAll();
// Fetch artist events
$event_count_stmt = $pdo->prepare("
SELECT COUNT(*)
FROM events
WHERE creator_id = ?
AND status = 'published'
AND (start_date IS NULL OR start_date >= DATE_SUB(NOW(), INTERVAL 1 DAY))
");
$event_count_stmt->execute([$artist_id]);
$artist['events_count'] = (int)$event_count_stmt->fetchColumn();
$events_stmt = $pdo->prepare("
SELECT
e.id,
e.title,
e.event_type,
e.start_date,
e.end_date,
e.location,
e.venue_name,
e.cover_image,
e.banner_image,
e.max_attendees,
e.status,
e.creator_id,
e.is_private_party,
e.party_password,
COALESCE(SUM(CASE WHEN et.status IN ('pending','confirmed') THEN 1 ELSE 0 END), 0) as tickets_sold
FROM events e
LEFT JOIN event_tickets et ON e.id = et.event_id
WHERE e.creator_id = ?
AND e.status = 'published'
AND (e.start_date IS NULL OR e.start_date >= DATE_SUB(NOW(), INTERVAL 1 DAY))
GROUP BY e.id
ORDER BY e.start_date ASC
LIMIT 6
");
$events_stmt->execute([$artist_id]);
$all_artist_events = $events_stmt->fetchAll(PDO::FETCH_ASSOC);
// Filter out private party events unless user has password access
$viewing_user_id = $_SESSION['user_id'] ?? null;
$artist_events = [];
foreach ($all_artist_events as $event) {
// Only check if columns exist (they might not exist if migration hasn't run)
$is_private_party = false;
$has_password = false;
if (isset($event['is_private_party'])) {
$is_private_party = ((int)$event['is_private_party'] === 1 || $event['is_private_party'] === true);
}
if (isset($event['party_password']) && !empty(trim($event['party_password']))) {
$has_password = true;
}
// Only filter if BOTH conditions are met: is private AND has password
if ($is_private_party && $has_password) {
// Check if viewing user is the creator (artist)
$is_creator = $viewing_user_id && $viewing_user_id == $event['creator_id'];
if ($is_creator) {
// Creator can always see their events
$artist_events[] = $event;
} else {
// Check if user has valid password access
$has_access = isset($_SESSION['party_access_' . $event['id']]) &&
isset($_SESSION['party_access_time_' . $event['id']]) &&
(time() - $_SESSION['party_access_time_' . $event['id']]) < 3600; // 1 hour access
if ($has_access) {
// User has entered correct password, show event
$artist_events[] = $event;
}
// Otherwise, don't include this event in the list
}
} else {
// Not a private party event, show it
$artist_events[] = $event;
}
}
// Check if current user is following this artist
$is_following = false;
if (isset($_SESSION['user_id'])) {
$stmt = $pdo->prepare("SELECT COUNT(*) FROM user_follows WHERE follower_id = ? AND following_id = ?");
$stmt->execute([$_SESSION['user_id'], $artist_id]);
$is_following = $stmt->fetchColumn() > 0;
}
// Check if current user is friends with this artist and friend request status
$is_friend = false;
$friend_status = 'not_friends'; // 'not_friends', 'pending', 'friends'
if (isset($_SESSION['user_id'])) {
// Check for accepted friendship
$stmt = $pdo->prepare("SELECT COUNT(*) FROM user_friends WHERE status = 'accepted' AND ((user_id = ? AND friend_id = ?) OR (user_id = ? AND friend_id = ?))");
$stmt->execute([$_SESSION['user_id'], $artist_id, $artist_id, $_SESSION['user_id']]);
$is_friend = $stmt->fetchColumn() > 0;
if ($is_friend) {
$friend_status = 'friends';
} else {
// Check for pending friend request (either direction)
$stmt = $pdo->prepare("SELECT COUNT(*) FROM user_friends WHERE status = 'pending' AND ((user_id = ? AND friend_id = ?) OR (user_id = ? AND friend_id = ?))");
$stmt->execute([$_SESSION['user_id'], $artist_id, $artist_id, $_SESSION['user_id']]);
$has_pending = $stmt->fetchColumn() > 0;
if ($has_pending) {
$friend_status = 'pending';
}
}
}
// Create artist_ratings table if it doesn't exist
try {
$pdo->exec("
CREATE TABLE IF NOT EXISTS artist_ratings (
id INT AUTO_INCREMENT PRIMARY KEY,
artist_id INT NOT NULL,
user_id INT NOT NULL,
rating TINYINT UNSIGNED NOT NULL,
comment TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY unique_rating (artist_id, user_id),
FOREIGN KEY (artist_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
");
} catch (Exception $e) {
error_log("Failed to create artist_ratings table: " . $e->getMessage());
}
// Fetch rating data regardless of whether table creation succeeded (table should already exist in production)
$rating_stmt = $pdo->prepare("
SELECT
COALESCE(AVG(rating), 0) as average_rating,
COUNT(*) as rating_count
FROM artist_ratings
WHERE artist_id = ?
");
$rating_stmt->execute([$artist_id]);
$rating_data = $rating_stmt->fetch();
$artist['average_rating'] = $rating_data['average_rating'];
$artist['rating_count'] = $rating_data['rating_count'];
$artist['vote_score'] = ($artist['rating_count'] ?? 0) > 0 ? round((float)$artist['average_rating'], 1) : null;
$artist['leaderboard_rank'] = null;
$artist['leaderboard_rank_ordinal'] = null;
$artist['leaderboard_total'] = 0;
$artist['top_artist'] = null;
$artist['leaderboard_top_artists'] = [];
try {
$total_ranked_stmt = $pdo->query("SELECT COUNT(DISTINCT artist_id) as total_ranked FROM artist_ratings WHERE rating IS NOT NULL");
$total_ranked = $total_ranked_stmt ? (int)($total_ranked_stmt->fetchColumn() ?? 0) : 0;
$artist['leaderboard_total'] = $total_ranked;
$top_artist_stmt = $pdo->query("
SELECT ar.artist_id, u.name as artist_name, AVG(ar.rating) as avg_rating, COUNT(*) as vote_count
FROM artist_ratings ar
JOIN users u ON ar.artist_id = u.id
GROUP BY ar.artist_id, u.name
HAVING vote_count > 0
ORDER BY avg_rating DESC, vote_count DESC
LIMIT 1
");
$top_artist_data = $top_artist_stmt ? $top_artist_stmt->fetch(PDO::FETCH_ASSOC) : false;
if ($top_artist_data) {
$top_artist_data['avg_rating'] = round((float)$top_artist_data['avg_rating'], 1);
$artist['top_artist'] = $top_artist_data;
}
$leaderboard_stmt = $pdo->query("
SELECT
ar.artist_id,
u.name as artist_name,
up.location,
AVG(ar.rating) as avg_rating,
COUNT(*) as vote_count
FROM artist_ratings ar
JOIN users u ON ar.artist_id = u.id
LEFT JOIN user_profiles up ON up.user_id = u.id
GROUP BY ar.artist_id, u.name, up.location
HAVING vote_count > 0
ORDER BY avg_rating DESC, vote_count DESC
LIMIT 5
");
$artist['leaderboard_top_artists'] = $leaderboard_stmt ? array_map(function($row) {
$row['avg_rating'] = round((float)$row['avg_rating'], 1);
return $row;
}, $leaderboard_stmt->fetchAll(PDO::FETCH_ASSOC)) : [];
if (($artist['rating_count'] ?? 0) > 0) {
$rank_stmt = $pdo->prepare("
SELECT COUNT(*) + 1 AS rank_position
FROM (
SELECT artist_id, AVG(rating) AS avg_rating, COUNT(*) AS vote_count
FROM artist_ratings
GROUP BY artist_id
) ranking
WHERE avg_rating > :avg_rating
OR (avg_rating = :avg_rating AND vote_count > :vote_count)
");
$rank_stmt->execute([
':avg_rating' => $artist['average_rating'],
':vote_count' => $artist['rating_count']
]);
$rank_position = $rank_stmt->fetchColumn();
if ($rank_position) {
$artist['leaderboard_rank'] = (int)$rank_position;
$artist['leaderboard_rank_ordinal'] = ssp_format_ordinal($artist['leaderboard_rank']);
}
}
} catch (Exception $e) {
error_log('Failed to compute artist leaderboard data: ' . $e->getMessage());
}
// Calculate monthly listeners and total track plays
$monthly_stmt = $pdo->prepare("
SELECT
COALESCE(COUNT(*), 0) as monthly_listeners
FROM track_plays tp
JOIN music_tracks mt ON tp.track_id = mt.id
WHERE mt.user_id = ? AND tp.played_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
");
$monthly_stmt->execute([$artist_id]);
$monthly_data = $monthly_stmt->fetch();
$total_plays_stmt = $pdo->prepare("
SELECT
COALESCE(COUNT(*), 0) as total_track_plays
FROM track_plays tp
JOIN music_tracks mt ON tp.track_id = mt.id
WHERE mt.user_id = ?
");
$total_plays_stmt->execute([$artist_id]);
$total_plays_data = $total_plays_stmt->fetch();
$artist['monthly_listeners'] = $monthly_data['monthly_listeners'];
$artist['total_track_plays'] = $total_plays_data['total_track_plays'];
// Translate badge after fetching
if (!empty($artist['badge'])) {
$badge_text = $artist['badge'];
// Extract emoji and translate text
if (strpos($badge_text, 'Pro Creator') !== false) {
$artist['badge'] = 'đĩ ' . t('artists.badge_pro_creator');
} elseif (strpos($badge_text, 'Rising Star') !== false) {
$artist['badge'] = 'â ' . t('artists.badge_rising_star');
} elseif (strpos($badge_text, 'Active Artist') !== false) {
$artist['badge'] = 'đĨ ' . t('artists.badge_active_artist');
} elseif (strpos($badge_text, 'New Artist') !== false) {
$artist['badge'] = 'đŧ ' . t('artists.badge_new_artist');
} elseif (strpos($badge_text, 'Member') !== false) {
$artist['badge'] = 'đ¤ ' . t('artists.badge_member');
}
}
// Set page variables
$page_title = $artist['username'] . ' - ' . t('artist_profile.page_title_suffix');
$page_description = str_replace(':username', $artist['username'], t('artist_profile.page_description'));
$current_page = 'artist_profile';
// Set OG image from artist profile image
$base_url = 'https://soundstudiopro.com';
$page_url = $base_url . (isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/artist_profile.php?id=' . $artist_id);
// Get profile image and convert to absolute URL for Open Graph
$profile_image_url = $artist['profile_image'] ?? null;
if (!empty($profile_image_url) && $profile_image_url !== 'null' && $profile_image_url !== 'NULL') {
// If it's already an absolute URL, use it as-is
if (strpos($profile_image_url, 'http://') === 0 || strpos($profile_image_url, 'https://') === 0) {
$og_image = $profile_image_url;
} else {
// Convert relative path to absolute URL
$clean_path = ltrim($profile_image_url, '/');
$og_image = $base_url . '/' . $clean_path;
}
} else {
// Fallback to default OG image if no profile image
$og_image = $base_url . '/assets/images/og-image.png';
}
// Set OG variables for header.php
$og_type = 'profile';
$og_url = $page_url;
$og_title = $page_title;
$og_description = $page_description;
$twitter_title = $page_title;
$twitter_description = $page_description;
$twitter_image = $og_image;
$canonical_url = $page_url;
// Now include header AFTER all redirects are handled
if ($is_ajax) {
echo '<div class="container" id="pageContainer">';
} else {
include 'includes/header.php';
}
?>
<!-- Clean Artist Profile Styles -->
<style>
/* Artist Hero Section */
.artist-hero {
background: linear-gradient(135deg, #0a0a0a 0%, #1a1a1a 50%, #0a0a0a 100%);
position: relative;
overflow: hidden;
padding: 6rem 0;
margin-bottom: 0;
}
.artist-hero::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(circle at 30% 20%, rgba(102, 126, 234, 0.1) 0%, transparent 50%),
radial-gradient(circle at 70% 80%, rgba(118, 75, 162, 0.1) 0%, transparent 50%);
pointer-events: none;
}
.hero-content {
max-width: 120rem;
margin: 0 auto;
position: relative;
z-index: 2;
padding: 0 2rem;
}
/* Cover Image */
.cover-image-container {
position: relative;
height: 300px;
border-radius: 20px;
overflow: hidden;
margin-bottom: -100px;
z-index: 1;
}
.cover-image {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center center; /* Adjust this to reposition the image */
}
/* Cover Image Reposition Controls */
.cover-reposition-controls {
position: absolute;
top: 15px;
right: 15px;
display: flex;
gap: 10px;
z-index: 10;
}
.reposition-btn,
.save-position-btn,
.cancel-position-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
border: none;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
color: #fff;
backdrop-filter: blur(10px);
}
.reposition-btn {
background: rgba(52, 152, 219, 0.8);
border: 1px solid rgba(52, 152, 219, 0.3);
}
.save-position-btn {
background: rgba(46, 204, 113, 0.8);
border: 1px solid rgba(46, 204, 113, 0.3);
}
.cancel-position-btn {
background: rgba(231, 76, 60, 0.8);
border: 1px solid rgba(231, 76, 60, 0.3);
}
.reposition-btn:hover,
.save-position-btn:hover,
.cancel-position-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
/* Draggable Cover Image State */
.cover-image-container.repositioning .cover-image {
cursor: grab;
transition: none;
}
.cover-image-container.repositioning .cover-image:active {
cursor: grabbing;
}
.cover-image-container.repositioning .cover-upload-overlay {
display: none;
}
/* Cover Image Positioning Options - Add these classes to your cover image */
.cover-image.position-top {
object-position: center top;
}
.cover-image.position-bottom {
object-position: center bottom;
}
.cover-image.position-left {
object-position: left center;
}
.cover-image.position-right {
object-position: right center;
}
.cover-image.position-top-left {
object-position: left top;
}
.cover-image.position-top-right {
object-position: right top;
}
.cover-image.position-bottom-left {
object-position: left bottom;
}
.cover-image.position-bottom-right {
object-position: right bottom;
}
/* Profile Image Reposition Controls */
.profile-reposition-controls {
position: absolute;
bottom: 15px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 6px;
z-index: 15;
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
}
.profile-image-container:hover .profile-reposition-controls {
opacity: 1;
pointer-events: auto;
}
.profile-reposition-btn,
.save-profile-position-btn,
.cancel-profile-position-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
padding: 6px 12px;
border: none;
border-radius: 15px;
cursor: pointer;
font-size: 11px;
font-weight: 600;
transition: all 0.3s ease;
white-space: nowrap;
backdrop-filter: blur(10px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.profile-reposition-btn {
background: rgba(102, 126, 234, 0.9);
color: white;
}
.save-profile-position-btn {
background: rgba(16, 185, 129, 0.9);
color: white;
}
.cancel-profile-position-btn {
background: rgba(239, 68, 68, 0.9);
color: white;
}
.profile-reposition-btn:hover,
.save-profile-position-btn:hover,
.cancel-profile-position-btn:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
.profile-reposition-btn i,
.save-profile-position-btn i,
.cancel-profile-position-btn i {
font-size: 10px;
}
/* Draggable Profile Image State */
.profile-image-container.repositioning .profile-image {
cursor: grab;
transition: none;
box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.7), 0 20px 40px rgba(0, 0, 0, 0.3);
animation: pulse-border 1.5s ease-in-out infinite;
}
@keyframes pulse-border {
0%, 100% { box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.7), 0 20px 40px rgba(0, 0, 0, 0.3); }
50% { box-shadow: 0 0 0 6px rgba(102, 126, 234, 0.5), 0 20px 40px rgba(0, 0, 0, 0.3); }
}
.profile-image-container.repositioning .profile-image:active {
cursor: grabbing;
}
.profile-image-container.repositioning .profile-upload-overlay {
display: none !important;
opacity: 0 !important;
}
.profile-image-container.repositioning .profile-reposition-controls {
opacity: 1;
pointer-events: auto;
}
.cover-placeholder {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 25%, #16213e 50%, #0f3460 75%, #0a0a0a 100%);
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.cover-placeholder-pattern {
position: absolute;
width: 100%;
height: 100%;
opacity: 0.3;
z-index: 1;
}
.cover-placeholder-waveform {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 40%;
background: repeating-linear-gradient(
90deg,
transparent,
transparent 2px,
rgba(102, 126, 234, 0.3) 2px,
rgba(102, 126, 234, 0.3) 4px
);
background-size: 20px 100%;
animation: waveform 3s ease-in-out infinite;
}
.cover-placeholder-music-notes {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
gap: 1.5rem;
font-size: 2rem;
color: rgba(102, 126, 234, 0.4);
z-index: 1;
}
.cover-placeholder-music-notes i {
animation: floatMusic 4s ease-in-out infinite;
animation-delay: calc(var(--i) * 0.3s);
}
.cover-placeholder-music-notes i:nth-child(1) { --i: 0; }
.cover-placeholder-music-notes i:nth-child(2) { --i: 1; }
.cover-placeholder-music-notes i:nth-child(3) { --i: 2; }
.cover-placeholder-gradient-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.2) 0%, rgba(118, 75, 162, 0.2) 50%, rgba(240, 147, 251, 0.1) 100%);
z-index: 2;
}
/* Artist Info Section */
.artist-info-section {
display: flex;
align-items: flex-end;
gap: 3rem;
margin-bottom: 3rem;
flex-wrap: wrap;
position: relative;
z-index: 2;
}
.profile-image-container {
position: relative;
flex-shrink: 0;
z-index: 3;
width: 200px;
height: 200px;
overflow: visible;
}
.profile-image {
width: 200px;
height: 200px;
border-radius: 50%;
object-fit: cover;
border: 6px solid #1a1a1a;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
}
.profile-placeholder {
width: 200px;
height: 200px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea, #764ba2);
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
border: 6px solid #1a1a1a;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
}
.profile-initial {
font-size: 5rem;
font-weight: 700;
color: white;
text-align: center;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}
.profile-placeholder-pattern {
position: absolute;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}
.profile-placeholder-pattern i {
font-size: 3rem;
color: rgba(102, 126, 234, 0.6);
animation: pulse 2s ease-in-out infinite;
text-shadow: 0 0 20px rgba(102, 126, 234, 0.5);
}
.profile-placeholder-gradient-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.3) 0%, rgba(118, 75, 162, 0.3) 50%, rgba(240, 147, 251, 0.2) 100%);
border-radius: 50%;
z-index: 2;
}
/* Keyframe animations for placeholders */
@keyframes waveform {
0%, 100% { transform: translateY(0) scaleY(1); }
50% { transform: translateY(-10px) scaleY(1.2); }
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
opacity: 0.6;
}
50% {
transform: scale(1.1);
opacity: 1;
}
}
@keyframes floatMusic {
0%, 100% { transform: translateY(0) rotate(0deg); }
50% { transform: translateY(-20px) rotate(5deg); }
}
.artist-details {
flex: 1;
min-width: 300px;
}
.artist-name {
font-size: 3.5rem;
font-weight: 700;
color: white;
margin: 0 0 1rem 0;
text-shadow: 0 4px 8px rgba(0, 0, 0, 0.5);
}
.artist-badge {
display: inline-flex;
align-items: center;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
padding: 0.75rem 1.5rem;
border-radius: 25px;
font-size: 1rem;
font-weight: 600;
margin: 0;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
white-space: nowrap;
}
.donation-button {
display: inline-flex;
align-items: center;
gap: 0.5rem;
background: linear-gradient(135deg, #0070ba 0%, #003087 100%);
color: white;
padding: 0.75rem 1.5rem;
border-radius: 25px;
font-size: 1rem;
font-weight: 600;
margin: 0;
text-decoration: none;
border: none;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0, 112, 186, 0.4);
transition: all 0.3s ease;
font-family: inherit;
white-space: nowrap;
}
.donation-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 112, 186, 0.5);
background: linear-gradient(135deg, #003087 0%, #0070ba 100%);
}
.donation-button:active {
transform: translateY(0);
box-shadow: 0 2px 8px rgba(0, 112, 186, 0.3);
}
.donation-button i {
font-size: 1.1rem;
}
.artist-stats {
display: flex;
gap: 2rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
@media (max-width: 600px) {
.artist-stats.primary-stats {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
}
.artist-stats.primary-stats .stat-item {
background: rgba(0,0,0,0.35);
border-radius: 16px;
padding: 1rem 0.5rem;
}
}
.artist-stats.secondary-stats {
margin-top: -0.5rem;
}
.artist-stats .stat-item {
min-width: 120px;
}
.stat-item {
text-align: center;
color: #e0e0e0;
}
.stat-item.compact-stat {
min-width: 140px;
}
.stat-item.compact-stat .stat-number {
font-size: 1.3rem;
}
.stat-item.compact-stat .stat-hint {
font-size: 0.8rem;
}
.stat-number {
font-size: 1.5rem;
font-weight: 700;
color: white;
display: block;
}
.stat-label {
font-size: 0.9rem;
color: #a0a0a0;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stat-item.stat-highlight .stat-number {
color: #63b3ed;
font-size: 1.75rem;
}
.stat-item.rank-stat .stat-number {
color: #f6ad55;
}
.stat-item.top-artist-stat .stat-number {
color: #f687b3;
font-size: 1.6rem;
}
.stat-hint {
display: block;
margin-top: 0.3rem;
font-size: 0.85rem;
color: #d5d5ff;
text-transform: none;
letter-spacing: 0;
}
.stat-number-circle {
width: 80px;
height: 80px;
border-radius: 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 0 auto 0.5rem auto;
font-weight: 700;
color: white;
background: conic-gradient(#63b3ed 0% 50%, rgba(99, 179, 237, 0.15) 50% 100%);
border: 2px solid rgba(255, 255, 255, 0.05);
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.4);
}
.stat-number-circle small {
font-size: 0.65rem;
color: #e0e0e0;
}
.platform-leaderboard-card {
margin-top: 1rem;
padding: 1.25rem;
border-radius: 16px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.35);
}
.platform-leaderboard-card.is-self {
border-color: rgba(246, 173, 85, 0.5);
background: rgba(246, 173, 85, 0.08);
}
.leaderboard-pill {
display: inline-flex;
padding: 0.25rem 0.75rem;
border-radius: 999px;
font-size: 0.8rem;
letter-spacing: 0.05em;
text-transform: uppercase;
background: rgba(255, 255, 255, 0.12);
color: #e0e0e0;
margin-bottom: 0.75rem;
}
.leaderboard-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.leaderboard-name {
font-size: 1.3rem;
font-weight: 600;
color: white;
}
.leaderboard-name-link {
color: inherit;
text-decoration: none;
}
.leaderboard-name-link:hover {
text-decoration: underline;
}
.leaderboard-score {
font-size: 2rem;
font-weight: 700;
color: #f687b3;
}
.leaderboard-text {
margin: 0.25rem 0 0.75rem 0;
color: #cbd5f5;
font-size: 0.95rem;
}
.leaderboard-link {
display: inline-flex;
align-items: center;
gap: 0.35rem;
color: #63b3ed;
text-decoration: none;
font-weight: 600;
font-size: 0.95rem;
background: transparent;
border: none;
cursor: pointer;
}
.leaderboard-link::after {
content: 'â';
font-size: 1rem;
}
.leaderboard-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: none;
align-items: center;
justify-content: center;
z-index: 10000;
backdrop-filter: blur(6px);
}
.leaderboard-modal.active {
display: flex;
animation: fadeIn 0.3s ease;
}
.leaderboard-modal-content {
background: #0f0f13;
border-radius: 24px;
width: 90%;
max-width: 960px;
padding: 2rem;
box-shadow: 0 30px 80px rgba(0,0,0,0.6);
position: relative;
}
.leaderboard-modal-close {
position: absolute;
top: 1rem;
right: 1rem;
background: transparent;
border: none;
color: #fff;
font-size: 1.5rem;
cursor: pointer;
}
.leaderboard-map {
background: radial-gradient(circle at 20% 20%, rgba(99,179,237,0.2), transparent 60%),
radial-gradient(circle at 80% 30%, rgba(255,138,189,0.2), transparent 55%),
rgba(255,255,255,0.03);
border-radius: 20px;
padding: 1.5rem;
margin-top: 1.5rem;
position: relative;
overflow: hidden;
}
.leaderboard-map::after {
content: '';
position: absolute;
inset: 0;
background: repeating-linear-gradient(120deg, rgba(255,255,255,0.04), rgba(255,255,255,0.04) 2px, transparent 2px, transparent 8px);
opacity: 0.3;
mix-blend-mode: screen;
pointer-events: none;
}
.leaderboard-map-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
position: relative;
z-index: 2;
}
.leaderboard-map-card {
background: rgba(0, 0, 0, 0.4);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 16px;
padding: 1rem;
color: #f7fafc;
}
.leaderboard-map-card h4 {
margin: 0;
font-size: 1.05rem;
}
.leaderboard-map-card span {
display: block;
font-size: 0.85rem;
color: #cbd5f5;
}
/* Action Buttons */
.artist-actions {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.action-btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 25px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 0.5rem;
}
.follow-btn {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
}
.follow-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(102, 126, 234, 0.4);
}
.follow-btn.following {
background: linear-gradient(135deg, #48bb78, #38a169);
}
.message-btn {
background: rgba(255, 255, 255, 0.1);
color: white;
border: 2px solid rgba(255, 255, 255, 0.2);
}
.message-btn:hover {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.3);
}
.share-btn {
background: rgba(255, 255, 255, 0.1);
color: white;
border: 2px solid rgba(255, 255, 255, 0.2);
}
.share-btn:hover {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.3);
}
.friend-btn.friend {
background: linear-gradient(135deg, #48bb78, #38a169);
}
.friend-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
}
/* Message Modal Styles */
.message-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: 10000;
backdrop-filter: blur(5px);
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.message-modal-content {
background: #1a1a1a;
border-radius: 20px;
width: 90%;
max-width: 600px;
max-height: 80vh;
display: flex;
flex-direction: column;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
animation: slideUp 0.3s ease;
}
@keyframes slideUp {
from {
transform: translateY(30px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.message-modal-header {
padding: 1.5rem 2rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.message-modal-header h3 {
margin: 0;
color: white;
font-size: 1.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.message-modal-title-link {
color: white;
text-decoration: none;
display: flex;
align-items: center;
gap: 0.5rem;
transition: all 0.3s ease;
cursor: pointer;
padding: 0.25rem 0.5rem;
border-radius: 6px;
}
.message-modal-title-link:hover {
color: #667eea;
background: rgba(102, 126, 234, 0.1);
transform: translateX(2px);
}
.message-modal-title-link i {
transition: transform 0.3s ease;
}
.message-modal-title-link:hover i {
transform: scale(1.1);
}
.close-modal {
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-modal:hover {
background: rgba(255, 255, 255, 0.1);
color: white;
}
.message-modal-body {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
}
.message-history {
flex: 1;
padding: 1.5rem;
overflow-y: auto;
max-height: 400px;
display: flex;
flex-direction: column;
gap: 1rem;
}
.message-item {
display: flex;
flex-direction: column;
max-width: 70%;
animation: messageSlide 0.3s ease;
}
@keyframes messageSlide {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message-item.sent {
align-self: flex-end;
align-items: flex-end;
}
.message-item.received {
align-self: flex-start;
align-items: flex-start;
}
.message-content {
padding: 0.75rem 1rem;
border-radius: 15px;
word-wrap: break-word;
line-height: 1.5;
}
.message-item.sent .message-content {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border-bottom-right-radius: 5px;
}
.message-item.received .message-content {
background: rgba(255, 255, 255, 0.1);
color: white;
border-bottom-left-radius: 5px;
}
.message-time {
font-size: 0.75rem;
color: #a0a0a0;
margin-top: 0.25rem;
padding: 0 0.5rem;
}
.loading-messages,
.no-messages,
.error-message {
text-align: center;
color: #a0a0a0;
padding: 2rem;
font-style: italic;
}
.message-input-container {
padding: 1.5rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
gap: 1rem;
align-items: flex-end;
}
#messageInput {
flex: 1;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 15px;
padding: 0.75rem 1rem;
color: white;
font-size: 1rem;
resize: none;
font-family: inherit;
}
#messageInput:focus {
outline: none;
border-color: #667eea;
background: rgba(255, 255, 255, 0.08);
}
#messageInput::placeholder {
color: #666;
}
.send-message-btn {
padding: 0.75rem 1.5rem;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border: none;
border-radius: 15px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
transition: all 0.3s ease;
white-space: nowrap;
}
.send-message-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(102, 126, 234, 0.4);
}
.send-message-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Scrollbar styling for message history */
.message-history::-webkit-scrollbar {
width: 6px;
}
.message-history::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 10px;
}
.message-history::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 10px;
}
.message-history::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
/* Artist Bio Section */
.artist-bio-section {
background: #1a1a1a;
padding: 3rem 0;
margin-bottom: 3rem;
}
.bio-container {
max-width: 120rem;
margin: 0 auto;
padding: 0 2rem;
}
.bio-header {
text-align: center;
margin-bottom: 3rem;
}
.bio-title {
font-size: 2.5rem;
font-weight: 700;
color: white;
margin: 0 0 1rem 0;
}
.bio-subtitle {
font-size: 1.2rem;
color: #a0a0a0;
max-width: 600px;
margin: 0 auto;
}
.bio-content {
background: #2a2a2a;
border-radius: 20px;
padding: 3rem;
text-align: center;
max-width: 800px;
margin: 0 auto;
}
.bio-text {
font-size: 1.1rem;
line-height: 1.8;
color: #e0e0e0;
margin-bottom: 2rem;
}
.placeholder-text {
color: #a0a0a0;
font-style: italic;
}
/* Music Store Section */
.music-marketplace {
background: #1a1a1a;
padding: 3rem 0;
margin-bottom: 3rem;
}
.marketplace-container {
max-width: 120rem;
margin: 0 auto;
padding: 0 2rem;
}
.marketplace-header {
text-align: center;
margin-bottom: 3rem;
}
.marketplace-title {
font-size: 2.5rem;
font-weight: 700;
color: white;
margin: 0 0 1rem 0;
}
.marketplace-subtitle {
font-size: 1.2rem;
color: #a0a0a0;
margin: 0;
}
/* Music Grid */
.music-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
margin-bottom: 3rem;
}
.music-card {
background: #1e1e1e;
border-radius: 16px;
overflow: hidden;
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
border: 1px solid rgba(255, 255, 255, 0.06);
animation: cardSlideUp 0.5s ease both;
}
@keyframes cardSlideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.music-card:nth-child(1) { animation-delay: 0.05s; }
.music-card:nth-child(2) { animation-delay: 0.1s; }
.music-card:nth-child(3) { animation-delay: 0.15s; }
.music-card:nth-child(4) { animation-delay: 0.2s; }
.music-card:nth-child(5) { animation-delay: 0.25s; }
.music-card:nth-child(6) { animation-delay: 0.3s; }
.music-card:hover {
transform: translateY(-8px) scale(1.01);
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(102, 126, 234, 0.3);
border-color: rgba(102, 126, 234, 0.4);
}
/* Image wrapper with overlay */
.music-artwork {
position: relative;
width: 100%;
padding-top: 100%;
overflow: hidden;
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
}
.track-cover-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s ease;
}
.music-card:hover .track-cover-image {
transform: scale(1.08);
}
.artwork-placeholder {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
font-size: 4rem;
font-weight: 700;
text-transform: uppercase;
}
/* Hover overlay with play button */
.artwork-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.85) 0%, rgba(118, 75, 162, 0.85) 100%);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: all 0.4s ease;
}
.music-card:hover .artwork-overlay {
opacity: 1;
}
.overlay-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.6rem;
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.85);
}
.music-card:hover .overlay-play-btn {
transform: scale(1);
}
.overlay-play-btn:hover {
transform: scale(1.12) !important;
box-shadow: 0 15px 40px rgba(0,0,0,0.4);
background: white;
}
.overlay-play-btn.playing {
background: #48bb78;
color: white;
border-color: rgba(72, 187, 120, 0.5);
}
/* Duration badge on image */
.track-duration-badge {
position: absolute;
bottom: 12px;
right: 12px;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(8px);
color: white;
padding: 6px 12px;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.5px;
z-index: 5;
}
.sale-badge {
position: absolute;
top: 12px;
left: 12px;
background: linear-gradient(135deg, #ef4444, #dc2626);
color: white;
padding: 6px 12px;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 700;
z-index: 10;
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4);
}
/* Card content */
.music-card .card-header {
display: block;
margin-bottom: 0;
}
.music-info {
padding: 1.25rem;
}
.music-title {
font-size: 1.1rem;
font-weight: 600;
color: white;
margin: 0 0 0.5rem 0;
line-height: 1.4;
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.music-title .track-title-link {
color: inherit;
text-decoration: none;
transition: color 0.2s ease;
cursor: pointer;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
min-width: 0;
}
.music-title .track-title-link:hover {
color: #667eea;
}
.for-sale-chip {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 5px 12px;
background: linear-gradient(135deg, rgba(46, 204, 113, 0.2), rgba(39, 174, 96, 0.25));
color: #2ecc71;
border-radius: 14px;
font-size: 0.85rem;
font-weight: 700;
font-weight: 600;
white-space: nowrap;
border: 1px solid rgba(46, 204, 113, 0.25);
flex-shrink: 0;
}
.music-meta {
display: flex;
gap: 0.5rem;
margin-bottom: 0.75rem;
flex-wrap: wrap;
}
.genre-tag, .mood-tag {
background: rgba(102, 126, 234, 0.15);
color: #a5b4fc;
padding: 4px 10px;
text-decoration: none;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
border: 1px solid rgba(102, 126, 234, 0.2);
}
/* 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;
}
/* 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;
}
.ranking-detail-item i {
font-size: 0.7rem;
color: #fbbf24;
}
/* Integrated Voting Button */
.btn-vote-track-integrated {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
color: white;
padding: 0.5rem 0.75rem;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
font-size: 0.8rem;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
display: flex;
align-items: center;
gap: 0.4rem;
white-space: nowrap;
}
.btn-vote-track-integrated:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.5);
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
}
.btn-vote-track-integrated:active {
transform: translateY(0);
box-shadow: 0 1px 6px rgba(102, 126, 234, 0.3);
}
@media (max-width: 768px) {
.card-track-ranking > div:first-child {
flex-direction: column !important;
gap: 0.75rem !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: 0.75rem !important;
width: 100% !important;
align-items: center !important;
padding-top: 0.75rem !important;
width: 100% !important;
align-items: center !important;
}
}
.music-details {
display: flex;
gap: 1rem;
color: #888;
font-size: 0.8rem;
}
.music-details span {
display: flex;
align-items: center;
gap: 0.4rem;
}
/* Stats bar */
.card-footer {
padding: 0 1.25rem 1rem;
}
.track-stats {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-bottom: 0.75rem;
}
.track-stats .stat-item {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.4rem 0.75rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 20px;
color: #888;
font-size: 0.8rem;
transition: all 0.2s ease;
}
.track-stats .stat-item:hover {
background: rgba(255, 255, 255, 0.1);
color: #ccc;
}
.track-stats .stat-item i {
font-size: 0.75rem;
}
/* Purchase section */
.card-purchase {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem 1.25rem;
background: rgba(255, 255, 255, 0.03);
border-top: 1px solid rgba(255, 255, 255, 0.06);
}
.price-display {
flex: 1;
}
.current-price {
font-size: 1.6rem;
font-weight: 800;
color: #fff;
letter-spacing: -0.5px;
background: linear-gradient(135deg, #fff, #e2e8f0);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.original-price {
font-size: 0.9rem;
color: #666;
text-decoration: line-through;
margin-bottom: 2px;
}
.add-to-cart-btn {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border: none;
padding: 0.7rem 1.25rem;
border-radius: 12px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.3);
}
.add-to-cart-btn:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.4);
}
.add-to-cart-btn:disabled {
background: linear-gradient(135deg, #10b981, #059669);
cursor: not-allowed;
transform: none;
box-shadow: 0 4px 15px rgba(16, 185, 129, 0.3);
}
.wishlist-btn {
width: 44px;
height: 44px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.12);
color: #888;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
font-size: 1.1rem;
}
.wishlist-btn:hover {
background: rgba(239, 68, 68, 0.15);
border-color: rgba(239, 68, 68, 0.3);
color: #ef4444;
}
.wishlist-btn.active {
background: rgba(239, 68, 68, 0.2);
border-color: rgba(239, 68, 68, 0.4);
color: #ef4444;
}
/* License link */
.license-info {
text-align: center;
padding: 0.75rem 1.25rem 1rem;
}
.license-badge {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.4rem 0.75rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 20px;
color: #666;
font-size: 0.75rem;
text-decoration: none;
transition: all 0.2s ease;
}
.license-badge:hover {
background: rgba(255, 255, 255, 0.1);
color: #888;
}
.original-price {
font-size: 0.95rem;
color: rgba(255, 255, 255, 0.6);
text-decoration: line-through;
}
/* Old button styles removed - now defined in card design section */
.add-to-cart-btn.added {
background: linear-gradient(135deg, #38a169 0%, #2f855a 100%);
pointer-events: none;
}
/* Non-card wishlist buttons (keep for profile actions) */
.profile-wishlist-btn {
background: rgba(255, 255, 255, 0.1);
color: white;
border: 2px solid rgba(255, 255, 255, 0.2);
padding: 0.75rem;
border-radius: 25px;
cursor: pointer;
transition: all 0.3s ease;
}
.profile-wishlist-btn:hover {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.3);
}
.wishlist-btn.active {
background: rgba(229, 62, 62, 0.15);
border-color: rgba(229, 62, 62, 0.4);
color: #fbd5d5;
}
/* Track Stats - Interactive buttons */
.track-stats .like-toggle {
cursor: pointer;
}
.track-stats .like-toggle:hover {
background: rgba(239, 68, 68, 0.15);
border-color: rgba(239, 68, 68, 0.3);
color: #ef4444;
}
.track-stats .like-toggle.liked {
background: rgba(239, 68, 68, 0.2);
border-color: rgba(239, 68, 68, 0.4);
color: #ef4444;
}
.track-stats .share-track-btn {
cursor: pointer;
}
.track-stats .share-track-btn:hover {
background: rgba(102, 126, 234, 0.15);
border-color: rgba(102, 126, 234, 0.3);
color: #667eea;
}
/* Rating stat styling */
.track-stats .stat-item[onclick*="Rating"] {
cursor: pointer;
}
.track-stats .stat-item[onclick*="Rating"]:hover {
background: rgba(251, 191, 36, 0.15);
border-color: rgba(251, 191, 36, 0.3);
}
/* Comment button styling */
.track-stats .comment-btn {
cursor: pointer;
}
.track-stats .comment-btn:hover {
background: rgba(59, 130, 246, 0.15);
border-color: rgba(59, 130, 246, 0.3);
color: #3b82f6;
}
/* Variations button styling */
.track-stats .variations-btn {
cursor: pointer;
color: #764ba2;
}
.track-stats .variations-btn:hover {
background: rgba(118, 75, 162, 0.15);
border-color: rgba(118, 75, 162, 0.3);
color: #764ba2;
}
.track-stats .variations-btn.active {
background: rgba(118, 75, 162, 0.2);
border-color: rgba(118, 75, 162, 0.4);
color: #a855f7;
}
/* Variations container */
.variations-container {
padding: 15px 20px;
background: rgba(15, 15, 15, 0.95);
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.variations-header {
font-weight: 600;
color: #e0e0e0;
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: rgba(255, 255, 255, 0.05);
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.1);
transition: all 0.2s ease;
}
.variation-item:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(118, 75, 162, 0.3);
}
.variation-info {
display: flex;
flex-direction: column;
gap: 2px;
}
.variation-title {
font-size: 0.85rem;
color: #e0e0e0;
font-weight: 500;
}
.variation-duration {
font-size: 0.75rem;
color: #888;
}
.variation-play-btn {
width: 36px;
height: 36px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea, #764ba2);
border: none;
color: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.variation-play-btn:hover {
transform: scale(1.1);
box-shadow: 0 4px 15px rgba(118, 75, 162, 0.4);
}
.variation-play-btn i {
font-size: 0.8rem;
}
.license-badge {
background: rgba(102, 126, 234, 0.2);
color: #667eea;
padding: 0.3rem 0.8rem;
border-radius: 15px;
font-size: 0.8rem;
font-weight: 500;
display: inline-flex;
align-items: center;
gap: 0.3rem;
text-decoration: none;
border: 1px solid rgba(102, 126, 234, 0.3);
}
.license-badge:hover {
border-color: rgba(102, 126, 234, 0.5);
background: rgba(102, 126, 234, 0.3);
}
/* Load More Section */
.load-more-section {
text-align: center;
}
.load-more-btn {
background: rgba(255, 255, 255, 0.1);
color: white;
border: 2px solid rgba(255, 255, 255, 0.2);
padding: 1rem 2rem;
border-radius: 25px;
font-size: 1.1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.load-more-btn:hover {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
}
/* Similar Artists Section */
.artist-sections-container {
max-width: 120rem;
margin: 0 auto;
padding: 0 2rem;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 3rem;
margin-bottom: 3rem;
}
.enhanced-sidebar-section {
background: #2a2a2a;
border-radius: 20px;
padding: 2rem;
border: 1px solid #3a3a3a;
}
.section-header-modern {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.header-content {
display: flex;
align-items: center;
gap: 1rem;
}
.header-icon {
font-size: 2rem;
}
.section-title-modern {
font-size: 1.5rem;
font-weight: 600;
color: white;
margin: 0;
}
.section-subtitle {
font-size: 1rem;
color: #a0a0a0;
margin: 0;
}
.header-action button {
background: rgba(255, 255, 255, 0.1);
color: white;
border: none;
padding: 0.5rem;
border-radius: 50%;
cursor: pointer;
transition: all 0.3s ease;
}
.header-action button:hover {
background: rgba(255, 255, 255, 0.2);
}
/* Artists Grid */
.artists-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
}
.artist-card-modern {
background: #3a3a3a;
border-radius: 15px;
padding: 1.5rem;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
border: 1px solid #4a4a4a;
}
.artist-card-modern:hover {
transform: translateY(-3px);
border-color: #667eea;
}
.artist-avatar-modern {
margin-bottom: 1rem;
display: flex;
justify-content: center;
}
.artist-avatar-img {
width: 60px;
height: 60px;
border-radius: 50%;
object-fit: cover;
margin: 0 auto;
border: 2px solid rgba(255, 255, 255, 0.1);
}
.avatar-placeholder {
width: 60px;
height: 60px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea, #764ba2);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 1.5rem;
font-weight: 600;
margin: 0 auto;
}
.artist-name-modern {
font-size: 1.1rem;
font-weight: 600;
color: white;
margin: 0 0 0.5rem 0;
}
.artist-meta {
display: flex;
justify-content: center;
gap: 1rem;
margin-bottom: 1rem;
color: #a0a0a0;
font-size: 0.8rem;
}
.meta-item {
display: flex;
align-items: center;
gap: 0.3rem;
}
.follow-btn-mini {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 20px;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 0.3rem;
margin: 0 auto;
}
.follow-btn-mini:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(102, 126, 234, 0.4);
}
/* Stats Grid */
.stats-grid-modern {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1.5rem;
}
.artist-events-wrapper {
margin-top: 3rem;
}
.artist-events-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
}
.artist-event-card {
background: rgba(15, 18, 35, 0.85);
border: 1px solid rgba(102, 126, 234, 0.2);
border-radius: 18px;
overflow: hidden;
cursor: pointer;
transition: transform 0.3s ease, box-shadow 0.3s ease;
display: flex;
flex-direction: column;
}
.artist-event-card:hover {
transform: translateY(-6px);
box-shadow: 0 18px 40px rgba(102, 126, 234, 0.3);
border-color: rgba(102, 126, 234, 0.45);
}
.artist-event-card .event-cover {
height: 150px;
background-size: cover;
background-position: center;
position: relative;
}
.artist-event-card .event-cover::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(180deg, transparent 0%, rgba(5, 6, 10, 0.75) 100%);
}
.artist-event-card .event-cover .event-type {
position: absolute;
left: 1rem;
bottom: 1rem;
z-index: 2;
background: rgba(5, 6, 10, 0.7);
border-radius: 999px;
padding: 0.35rem 0.9rem;
font-size: 0.9rem;
text-transform: capitalize;
}
.artist-event-card .event-card-body {
padding: 1.5rem;
flex: 1;
}
.artist-event-card .event-card-body h4 {
margin: 0 0 0.55rem;
font-size: 1.6rem;
font-weight: 800;
color: #ffffff;
letter-spacing: 0.02em;
background: linear-gradient(120deg, #a5b4fc, #f8f5ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.artist-event-card .event-card-body p {
margin: 0 0 0.35rem;
color: #a0aec0;
font-size: 0.95rem;
}
.artist-event-card .event-card-body .event-date-line {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.55rem 0.85rem;
border-radius: 12px;
background: rgba(102, 126, 234, 0.18);
border: 1px solid rgba(102, 126, 234, 0.35);
color: #eef1ff;
font-size: 1.08rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
margin-bottom: 0.5rem;
box-shadow: 0 10px 25px rgba(14, 18, 40, 0.45);
}
.artist-event-card .event-card-body p i,
.artist-event-card .event-card-body .event-date-line i {
margin-right: 0.4rem;
color: #b3c4ff;
font-size: 1rem;
}
.artist-event-card .event-tickets-meta {
margin-top: 1rem;
display: flex;
justify-content: space-between;
gap: 1rem;
}
.artist-event-card .event-ticket-chip {
flex: 1;
padding: 0.85rem 1rem;
border-radius: 14px;
background: rgba(21, 24, 48, 0.85);
border: 1px solid rgba(138, 150, 255, 0.25);
display: flex;
flex-direction: column;
gap: 0.2rem;
box-shadow: 0 12px 30px rgba(10, 12, 28, 0.4);
}
.artist-event-card .event-ticket-chip.sold {
background: linear-gradient(135deg, rgba(76, 201, 240, 0.12), rgba(129, 140, 248, 0.16));
border-color: rgba(129, 140, 248, 0.6);
}
.artist-event-card .event-ticket-chip.remaining {
background: linear-gradient(135deg, rgba(129, 248, 193, 0.12), rgba(56, 209, 143, 0.15));
border-color: rgba(56, 209, 143, 0.6);
}
.artist-event-card .event-ticket-chip small {
font-size: 0.85rem;
color: rgba(226, 232, 240, 0.8);
letter-spacing: 0.05em;
text-transform: uppercase;
}
.artist-event-card .event-ticket-chip strong {
font-size: 1.35rem;
font-weight: 800;
color: #fff;
letter-spacing: 0.03em;
}
.artist-event-card .event-card-footer {
padding: 0 1.5rem 1.5rem;
}
.artist-event-card .event-card-footer button {
width: 100%;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 10px;
padding: 0.85rem 1rem;
background: transparent;
color: #e2e8f0;
font-weight: 600;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
}
.stat-card {
background: #3a3a3a;
border-radius: 15px;
padding: 1.5rem;
text-align: center;
border: 1px solid #4a4a4a;
}
.stat-icon {
font-size: 2rem;
color: #667eea;
margin-bottom: 1rem;
}
.stat-number {
font-size: 1.5rem;
font-weight: 700;
color: white;
margin-bottom: 0.5rem;
}
.stat-label {
font-size: 0.9rem;
color: #a0a0a0;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stat-trend {
display: flex;
align-items: center;
justify-content: center;
gap: 0.3rem;
margin-top: 0.5rem;
font-size: 0.8rem;
font-weight: 500;
}
.stat-trend.positive {
color: #48bb78;
}
.stat-trend.negative {
color: #f56565;
}
.stat-trend.neutral {
color: #a0a0a0;
}
/* Empty States */
.empty-store, .empty-state {
text-align: center;
padding: 3rem;
color: #a0a0a0;
}
.empty-icon {
font-size: 3rem;
margin-bottom: 1rem;
}
.empty-store h3, .empty-state h4 {
font-size: 1.5rem;
color: white;
margin: 0 0 1rem 0;
}
.empty-store p, .empty-state p {
font-size: 1rem;
margin: 0 0 2rem 0;
}
.btn-primary {
background: linear-gradient(135deg, #667eea, #764ba2);
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;
text-decoration: none;
display: inline-block;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(102, 126, 234, 0.4);
}
/* Responsive Design */
@media (max-width: 768px) {
.artist-hero {
padding: 2rem 0 4rem 0;
}
.hero-content {
padding: 0 1rem;
}
/* Cover Image Mobile Fixes */
.cover-image-container {
height: 200px;
border-radius: 12px;
margin-bottom: -60px;
}
.cover-reposition-controls {
top: 10px;
right: 10px;
gap: 5px;
}
.reposition-btn,
.save-position-btn,
.cancel-position-btn {
width: 32px;
height: 32px;
font-size: 14px;
padding: 0;
}
/* Artist Info Section Mobile */
.artist-info-section {
flex-direction: column;
text-align: center;
gap: 1.5rem;
align-items: center;
}
.profile-image-container {
margin: 0 auto;
width: 150px;
height: 150px;
}
.profile-image,
.profile-placeholder {
width: 150px;
height: 150px;
border: 4px solid #1a1a1a;
}
/* Profile Reposition Controls Mobile */
.profile-reposition-controls {
bottom: 10px;
gap: 4px;
}
.profile-reposition-btn,
.save-profile-position-btn,
.cancel-profile-position-btn {
padding: 5px 8px;
font-size: 10px;
}
.profile-reposition-btn span {
display: none;
}
.artist-details {
min-width: 100%;
width: 100%;
}
.artist-name {
font-size: 2.2rem;
margin-bottom: 0.5rem;
}
.artist-badge {
font-size: 0.9rem;
padding: 0.4rem 1.2rem;
margin-bottom: 0.8rem;
}
.donation-button {
font-size: 0.9rem;
padding: 0.6rem 1.2rem;
margin: 0.8rem 0;
}
/* Stats Mobile */
.artist-stats {
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
}
.stat-item {
min-width: calc(50% - 0.5rem);
flex: 0 0 calc(50% - 0.5rem);
}
.stat-number {
font-size: 1.3rem;
}
.stat-label {
font-size: 0.9rem;
}
/* Action Buttons Mobile */
.artist-actions {
flex-wrap: wrap;
justify-content: center;
gap: 0.5rem;
}
.action-btn {
padding: 10px 14px;
font-size: 13px;
min-height: 44px; /* Touch target size */
flex: 0 0 auto;
}
/* Genre Tags Mobile */
.genre-tags {
justify-content: center;
}
.genre-tag {
font-size: 11px;
padding: 5px 10px;
}
/* Music Grid Mobile */
.music-grid {
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.music-info {
padding: 1rem;
}
.music-title {
font-size: 0.95rem;
}
.music-meta {
gap: 0.4rem;
}
.genre-tag, .mood-tag {
font-size: 0.65rem;
padding: 3px 8px;
}
.card-footer {
padding: 0 1rem 0.75rem;
}
.track-stats {
gap: 0.3rem;
}
.track-stats .stat-item {
padding: 0.3rem 0.5rem;
font-size: 0.7rem;
}
.card-purchase {
padding: 0.75rem 1rem;
flex-wrap: wrap;
}
.current-price {
font-size: 1.3rem;
}
.for-sale-chip {
font-size: 0.8rem;
padding: 4px 10px;
}
.add-to-cart-btn {
padding: 0.6rem 1rem;
font-size: 0.8rem;
flex: 1;
}
.wishlist-btn {
width: 38px;
height: 38px;
min-width: 0;
}
/* Track Ranking Display Mobile */
.card-track-ranking {
padding: 0.6rem;
margin-top: 0.5rem;
}
.ranking-badge-compact {
gap: 0.4rem;
margin-bottom: 0.4rem;
}
.ranking-badge-compact i {
font-size: 0.9rem;
}
.ranking-label-compact {
font-size: 0.65rem;
}
.ranking-value-compact {
font-size: 1rem;
}
.ranking-out-of-compact {
font-size: 0.7rem;
}
.percentile-badge-compact {
padding: 0.2rem 0.5rem;
font-size: 0.65rem;
}
.ranking-details-compact {
gap: 0.4rem;
}
.ranking-detail-item {
padding: 0.25rem 0.5rem;
font-size: 0.65rem;
}
.ranking-detail-item i {
font-size: 0.65rem;
}
/* Old action layout styles removed - using new card design */
.music-artwork {
width: 100%;
height: 200px;
min-width: 100%;
margin-bottom: 1rem;
}
/* Artist Sections Mobile */
.artist-sections-container {
grid-template-columns: 1fr;
gap: 2rem;
padding: 0 1rem;
}
.marketplace-container,
.bio-container {
padding: 0 1rem;
}
.bio-content {
padding: 2rem 1.5rem;
}
.bio-title,
.marketplace-title {
font-size: 2rem;
}
/* Message Modal Mobile */
.message-modal-content {
width: 95%;
max-height: 90vh;
margin: 1rem;
}
.message-item {
max-width: 85%;
}
/* Profile Edit Modal Mobile */
.modal-content {
width: 95%;
max-height: 90vh;
margin: 1rem;
}
/* Prevent horizontal overflow */
html, body {
overflow-x: hidden;
}
* {
max-width: 100%;
}
/* Location and genre tags mobile */
.artist-location {
font-size: 13px;
justify-content: center;
}
/* Bio section mobile */
.bio-text {
font-size: 1rem;
line-height: 1.6;
}
/* Music details mobile */
.music-details {
flex-direction: column;
gap: 0.5rem;
align-items: flex-start;
}
/* Ensure all containers are responsive */
.hero-content,
.profile-cards-section,
.music-marketplace,
.artist-bio-section {
overflow-x: hidden;
}
/* Fix any fixed widths */
.artist-details {
max-width: 100%;
}
}
/* Extra Small Mobile Devices */
@media (max-width: 480px) {
.artist-hero {
padding: 1.5rem 0 3rem 0;
}
.cover-image-container {
height: 150px;
margin-bottom: -50px;
}
.profile-image-container {
width: 120px;
height: 120px;
}
.profile-image,
.profile-placeholder {
width: 120px;
height: 120px;
}
.profile-reposition-controls {
bottom: 8px;
}
.profile-reposition-btn,
.save-profile-position-btn,
.cancel-profile-position-btn {
padding: 4px 6px;
font-size: 9px;
}
.artist-name {
font-size: 1.8rem;
}
.artist-badge {
font-size: 0.8rem;
padding: 0.3rem 1rem;
}
.stat-item {
min-width: calc(50% - 0.25rem);
flex: 0 0 calc(50% - 0.25rem);
}
.stat-number {
font-size: 1.2rem;
}
.stat-label {
font-size: 0.8rem;
}
.action-btn {
padding: 8px 12px;
font-size: 12px;
min-height: 44px;
flex: 1 1 calc(50% - 0.25rem);
min-width: calc(50% - 0.25rem);
}
.artist-actions {
gap: 0.5rem;
}
/* Single column on very small screens */
.music-grid {
grid-template-columns: 1fr;
}
.overlay-play-btn {
width: 56px;
height: 56px;
font-size: 1.3rem;
}
.bio-content {
padding: 1.5rem 1rem;
}
.bio-title,
.marketplace-title {
font-size: 1.75rem;
}
.profile-cards-title {
font-size: 1.75rem;
}
.profile-card {
padding: 1.25rem;
width: 100% !important;
max-width: 100% !important;
min-height: auto;
}
.card-title {
font-size: 1.1rem;
}
/* Ensure cards are truly single column on small screens */
.profile-cards-grid {
grid-template-columns: 1fr !important;
}
.profile-cards-grid .profile-card {
grid-column: 1 !important;
grid-row: auto !important;
}
}
/* Genre Tags - INSPIRED BY BEATPORT & SPOTIFY */
.genre-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 15px 0;
}
.genre-tag {
background: rgba(255, 255, 255, 0.1);
color: #fff;
padding: 6px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
border: 1px solid rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
transition: all 0.3s ease;
cursor: pointer;
}
.genre-tag:hover {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.4);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.artist-location {
color: #b0b0b0;
font-size: 14px;
margin: 8px 0;
display: flex;
align-items: center;
gap: 5px;
}
/* Enhanced Action Buttons */
.artist-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 20px;
}
.action-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
border: none;
border-radius: 25px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
color: #fff;
}
.action-btn.radio-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.action-btn.follow-btn {
background: #e74c3c;
}
.action-btn.follow-btn.following {
background: #c0392b;
}
.action-btn.friend-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.action-btn.friend-btn.friend {
background: #27ae60;
}
.action-btn.message-btn,
.action-btn.share-btn {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.action-btn.edit-btn {
background: #f39c12;
}
.action-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
}
/* Image Upload Overlays */
.cover-upload-overlay,
.profile-upload-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
border-radius: inherit;
z-index: 5;
pointer-events: none;
}
.profile-upload-overlay {
border-radius: 50%;
}
.cover-image-container:hover .cover-upload-overlay,
.profile-image-container:hover .profile-upload-overlay {
opacity: 1;
pointer-events: auto;
}
.cover-image-container:hover .cover-upload-overlay,
.profile-image-container:hover .profile-upload-overlay {
opacity: 1;
}
.upload-label {
color: #fff;
text-align: center;
cursor: pointer;
padding: 10px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
transition: all 0.3s ease;
position: relative;
z-index: 6;
pointer-events: auto;
}
.upload-label:hover {
background: rgba(255, 255, 255, 0.2);
transform: scale(1.05);
}
.upload-label i {
display: block;
font-size: 24px;
margin-bottom: 5px;
}
.upload-label span {
font-size: 12px;
font-weight: 500;
}
/* Profile Cards Section - BEAUTIFUL STYLING FROM BACKUP */
.profile-cards-section {
background: linear-gradient(135deg, rgba(10, 10, 20, 0.95) 0%, rgba(25, 25, 45, 0.9) 100%);
padding: 4rem 0;
position: relative;
overflow: hidden;
border-radius: 20px;
margin: 2rem;
}
.profile-cards-container {
max-width: 120rem;
margin: 0 auto;
padding: 0 2rem;
}
.profile-cards-header {
text-align: center;
margin-bottom: 3rem;
}
.profile-cards-title {
font-size: 2.5rem;
font-weight: 700;
color: white !important;
margin: 0 0 1rem 0;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}
.profile-cards-subtitle {
font-size: 1.2rem;
color: #a0a0a0 !important;
margin: 0;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}
.profile-cards-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 2rem;
margin-bottom: 2rem;
}
/* Ensure proper layout for 8 cards (4 rows of 2) */
.profile-cards-grid .profile-card:nth-child(1),
.profile-cards-grid .profile-card:nth-child(2) {
grid-row: 1;
}
.profile-cards-grid .profile-card:nth-child(3),
.profile-cards-grid .profile-card:nth-child(4) {
grid-row: 2;
}
.profile-cards-grid .profile-card:nth-child(5),
.profile-cards-grid .profile-card:nth-child(6) {
grid-row: 3;
}
.profile-cards-grid .profile-card:nth-child(7),
.profile-cards-grid .profile-card:nth-child(8) {
grid-row: 4;
}
/* BEAUTIFUL CARD STYLING FROM BACKUP */
.profile-card {
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 20px;
padding: 2.5rem;
transition: all 0.4s ease;
position: relative;
overflow: hidden;
min-height: 200px;
display: flex;
flex-direction: column;
justify-content: space-between;
width: 100%;
box-sizing: border-box;
}
.profile-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, #667eea, #764ba2, #667eea);
background-size: 200% 100%;
animation: shimmer 3s ease-in-out infinite;
}
@keyframes shimmer {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
.profile-card:hover {
transform: translateY(-8px);
box-shadow: 0 20px 40px rgba(102, 126, 234, 0.2);
border-color: rgba(102, 126, 234, 0.3);
}
/* BEAUTIFUL CARD HEADER STYLING */
.card-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 2rem;
}
.card-icon {
font-size: 2rem;
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 12px;
background: rgba(255, 255, 255, 0.1);
}
.card-title {
font-size: 2rem;
font-weight: 700;
color: white !important;
margin: 0;
flex: 1;
display: flex;
align-items: center;
gap: 1rem;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}
.card-edit-icon {
font-size: 1.2rem;
color: #a0a0a0;
opacity: 0.7;
transition: all 0.3s ease;
cursor: pointer;
}
.profile-card:hover .card-edit-icon {
opacity: 1;
color: #667eea;
}
.card-content {
min-height: 120px;
display: flex;
align-items: center;
justify-content: center;
flex: 1;
padding: 1rem 0;
}
.card-text {
color: #e0e0e0 !important;
font-size: 1rem;
line-height: 1.6;
text-align: center;
}
.placeholder-content {
text-align: center;
color: #a0a0a0 !important;
}
/* BEAUTIFUL CONTENT STYLING FROM BACKUP */
.card-content {
color: #e0e0e0 !important;
min-height: 120px;
display: flex;
align-items: center;
justify-content: center;
flex: 1;
padding: 1rem 0;
}
.card-content * {
color: inherit !important;
}
/* Enhanced content styles for beautiful lists */
.highlights-list {
display: flex;
flex-direction: column;
gap: 1.2rem;
}
.highlight-item {
display: flex;
align-items: center;
gap: 1.2rem;
padding: 1.2rem;
background: rgba(255, 255, 255, 0.08);
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
transition: all 0.3s ease;
}
.highlight-item:hover {
background: rgba(255, 255, 255, 0.12);
transform: translateX(8px);
}
.highlight-item i {
color: #fbbf24;
font-size: 1.4rem;
flex-shrink: 0;
}
.highlight-item span {
color: #e2e8f0;
font-size: 1.4rem;
font-weight: 500;
line-height: 1.4;
}
.achievements-list {
display: flex;
flex-direction: column;
gap: 1.2rem;
}
.achievement-item {
display: flex;
align-items: center;
gap: 1.2rem;
padding: 1.2rem;
background: rgba(255, 255, 255, 0.08);
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
transition: all 0.3s ease;
}
.achievement-item:hover {
background: rgba(255, 255, 255, 0.12);
transform: translateX(8px);
}
.achievement-item i {
color: #f59e0b;
font-size: 1.4rem;
flex-shrink: 0;
}
.achievement-item span {
color: #e2e8f0;
font-size: 1.4rem;
font-weight: 500;
line-height: 1.4;
}
.influences-content,
.equipment-content {
color: #e2e8f0;
font-size: 1.5rem;
line-height: 1.7;
padding: 1.5rem;
background: rgba(255, 255, 255, 0.05);
border-radius: 15px;
border: 1px solid rgba(255, 255, 255, 0.08);
}
.bio-content {
color: #e2e8f0;
font-size: 1.5rem;
line-height: 1.7;
padding: 1.5rem;
background: rgba(255, 255, 255, 0.05);
border-radius: 15px;
border: 1px solid rgba(255, 255, 255, 0.08);
}
.statement-content {
color: #e2e8f0;
font-size: 1.5rem;
line-height: 1.7;
padding: 1.5rem;
background: rgba(255, 255, 255, 0.05);
border-radius: 15px;
border: 1px solid rgba(255, 255, 255, 0.08);
text-align: center;
font-style: italic;
}
/* Beautiful List Editor Modal Styling */
.list-edit-modal .modal-content {
max-width: 600px;
width: 90%;
}
.list-editor {
padding: 1rem 0;
}
.add-item-section {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
align-items: center;
}
.new-item-input {
flex: 1;
padding: 12px 16px;
border: 2px solid rgba(102, 126, 234, 0.3);
border-radius: 8px;
background: rgba(255, 255, 255, 0.05);
color: white;
font-size: 1rem;
transition: all 0.3s ease;
}
.new-item-input:focus {
outline: none;
border-color: rgba(102, 126, 234, 0.6);
background: rgba(255, 255, 255, 0.08);
}
.btn-add-item {
padding: 12px 20px;
background: linear-gradient(135deg, #667eea, #764ba2);
border: none;
border-radius: 8px;
color: white;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
}
.btn-add-item:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.3);
}
.items-list {
max-height: 300px;
overflow-y: auto;
margin-bottom: 1.5rem;
}
.list-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
margin-bottom: 8px;
transition: all 0.3s ease;
}
.list-item:hover {
background: rgba(255, 255, 255, 0.12);
border-color: rgba(255, 255, 255, 0.2);
}
.item-text {
color: #e2e8f0;
font-size: 1rem;
flex: 1;
margin-right: 1rem;
}
.btn-remove-item {
background: rgba(239, 68, 68, 0.2);
border: 1px solid rgba(239, 68, 68, 0.3);
border-radius: 6px;
color: #ef4444;
padding: 6px 10px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
min-width: 32px;
}
.btn-remove-item:hover {
background: rgba(239, 68, 68, 0.3);
border-color: rgba(239, 68, 68, 0.5);
transform: scale(1.1);
}
.list-help {
background: rgba(102, 126, 234, 0.1);
border: 1px solid rgba(102, 126, 234, 0.2);
border-radius: 8px;
padding: 1rem;
margin-top: 1rem;
}
.list-help p {
color: #a0aec0;
font-size: 0.9rem;
margin: 0;
line-height: 1.5;
}
.list-help strong {
color: #667eea;
}
/* Tag Editor Modal Styling */
.tag-edit-modal .modal-content {
max-width: 600px;
width: 90%;
}
.tag-editor {
padding: 1rem 0;
}
.add-tag-section {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
align-items: center;
}
.new-tag-input {
flex: 1;
padding: 12px 16px;
border: 2px solid rgba(102, 126, 234, 0.3);
border-radius: 8px;
background: rgba(255, 255, 255, 0.05);
color: white;
font-size: 1rem;
transition: all 0.3s ease;
}
.new-tag-input:focus {
outline: none;
border-color: rgba(102, 126, 234, 0.6);
background: rgba(255, 255, 255, 0.08);
}
.btn-add-tag {
padding: 12px 20px;
background: linear-gradient(135deg, #667eea, #764ba2);
border: none;
border-radius: 8px;
color: white;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
}
.btn-add-tag:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.3);
}
.tags-container {
max-height: 300px;
overflow-y: auto;
margin-bottom: 1.5rem;
}
.tag-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: rgba(102, 126, 234, 0.1);
border: 1px solid rgba(102, 126, 234, 0.2);
border-radius: 8px;
margin-bottom: 8px;
transition: all 0.3s ease;
}
.tag-item:hover {
background: rgba(102, 126, 234, 0.15);
border-color: rgba(102, 126, 234, 0.3);
}
.tag-text-input {
color: #e2e8f0;
font-size: 1rem;
flex: 1;
margin-right: 1rem;
background: #3a3a3a;
border: 1px solid #4a4a4a;
border-radius: 6px;
padding: 8px 12px;
outline: none;
transition: all 0.3s ease;
}
.tag-text-input:focus {
border-color: #667eea;
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
background: #404040;
}
.tag-text-input:hover {
border-color: #5a6fd8;
background: #404040;
}
.btn-remove-tag {
background: rgba(239, 68, 68, 0.2);
border: 1px solid rgba(239, 68, 68, 0.3);
border-radius: 6px;
color: #ef4444;
padding: 6px 10px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
min-width: 32px;
}
.btn-remove-tag:hover {
background: rgba(239, 68, 68, 0.3);
border-color: rgba(239, 68, 68, 0.5);
transform: scale(1.1);
}
.tag-help {
background: rgba(102, 126, 234, 0.1);
border: 1px solid rgba(102, 126, 234, 0.2);
border-radius: 8px;
padding: 1rem;
margin-top: 1rem;
}
.tag-help p {
color: #a0aec0;
font-size: 0.9rem;
margin: 0;
line-height: 1.5;
}
.tag-help strong {
color: #667eea;
}
/* Rating and Engagement Cards Styling */
.rating-card .rating-display {
text-align: center;
margin-bottom: 1.5rem;
}
.stars-container {
display: flex;
justify-content: center;
gap: 0.5rem;
margin-bottom: 1rem;
}
.stars-container i {
font-size: 1.5rem;
color: #facc15; /* bright yellow for filled stars */
text-shadow: 0 0 6px rgba(250, 204, 21, 0.5);
}
.stars-container .far {
color: #4b5563; /* neutral gray for empty stars */
}
.rating-text {
font-size: 2rem;
font-weight: 700;
color: white;
margin-bottom: 0.5rem;
}
.rating-count {
font-size: 0.9rem;
color: #a0aec0;
margin-bottom: 1.5rem;
}
.btn-rate {
background: linear-gradient(135deg, #667eea, #764ba2);
border: none;
color: white;
padding: 0.75rem 1.5rem;
border-radius: 25px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
width: 100%;
}
.btn-rate:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.engagement-card .engagement-display {
text-align: center;
margin-bottom: 1.5rem;
}
.engagement-bar {
position: relative;
height: 12px;
background: rgba(255, 255, 255, 0.1);
border-radius: 6px;
margin: 1rem 0;
overflow: hidden;
}
.engagement-fill {
height: 100%;
background: linear-gradient(90deg, #10b981, #059669);
border-radius: 6px;
transition: width 0.3s ease;
}
.engagement-text {
font-size: 2rem;
font-weight: 700;
color: white;
margin-bottom: 0.5rem;
}
.engagement-label {
font-size: 0.9rem;
color: #10b981;
font-weight: 600;
margin-bottom: 1.5rem;
}
.engagement-stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.stat-item {
text-align: center;
padding: 1rem;
background: rgba(255, 255, 255, 0.05);
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.stat-number {
display: block;
font-size: 1.5rem;
font-weight: 700;
color: white;
margin-bottom: 0.25rem;
}
.stat-label {
font-size: 0.8rem;
color: #a0aec0;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Rating Modal Styling */
.rating-input-section {
text-align: center;
margin-bottom: 2rem;
}
.stars-input {
display: flex;
justify-content: center;
gap: 1rem;
margin-bottom: 1rem;
}
.stars-input i {
font-size: 2.5rem;
color: #fbbf24;
cursor: pointer;
transition: all 0.3s ease;
}
.stars-input i:hover {
transform: scale(1.2);
color: #f59e0b;
}
.rating-feedback {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.rating-value {
font-size: 1.5rem;
font-weight: 700;
color: white;
}
.rating-label {
font-size: 1rem;
color: #a0aec0;
}
.rating-comment {
margin-top: 2rem;
}
.rating-comment label {
display: block;
color: #e2e8f0;
font-weight: 600;
margin-bottom: 0.5rem;
font-size: 1.1rem;
}
.rating-comment textarea {
width: 100%;
padding: 16px;
border: 2px solid rgba(102, 126, 234, 0.3);
border-radius: 8px;
background: rgba(255, 255, 255, 0.05);
color: white;
font-size: 1rem;
line-height: 1.6;
resize: vertical;
transition: all 0.3s ease;
}
.rating-comment textarea:focus {
outline: none;
border-color: rgba(102, 126, 234, 0.6);
background: rgba(255, 255, 255, 0.08);
}
/* Engagement Modal Styling */
.engagement-analytics {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
}
.analytics-item {
text-align: center;
padding: 1.5rem;
background: rgba(255, 255, 255, 0.05);
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.analytics-label {
font-size: 0.9rem;
color: #a0aec0;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 0.5rem;
}
.analytics-value {
font-size: 2rem;
font-weight: 700;
color: white;
margin-bottom: 0.5rem;
}
.analytics-trend {
font-size: 0.8rem;
font-weight: 600;
padding: 0.25rem 0.75rem;
border-radius: 15px;
display: inline-block;
}
.analytics-trend.positive {
background: rgba(16, 185, 129, 0.2);
color: #10b981;
border: 1px solid rgba(16, 185, 129, 0.3);
}
.analytics-trend.neutral {
background: rgba(156, 163, 175, 0.2);
color: #9ca3af;
border: 1px solid rgba(156, 163, 175, 0.3);
}
/* Bio Editor Modal Styling */
.bio-edit-modal .modal-content {
max-width: 700px;
width: 90%;
}
.bio-editor {
padding: 1rem 0;
}
.bio-input-section {
margin-bottom: 2rem;
}
.bio-input-section label {
display: block;
color: #e2e8f0;
font-weight: 600;
margin-bottom: 0.5rem;
font-size: 1.1rem;
}
.bio-input-section textarea {
width: 100%;
padding: 16px;
border: 2px solid rgba(102, 126, 234, 0.3);
border-radius: 8px;
background: rgba(255, 255, 255, 0.05);
color: white;
font-size: 1rem;
line-height: 1.6;
resize: vertical;
transition: all 0.3s ease;
}
.bio-input-section textarea:focus {
outline: none;
border-color: rgba(102, 126, 234, 0.6);
background: rgba(255, 255, 255, 0.08);
}
.char-count {
text-align: right;
margin-top: 0.5rem;
font-size: 0.9rem;
color: #6b7280;
}
.bio-help {
background: rgba(102, 126, 234, 0.1);
border: 1px solid rgba(102, 126, 234, 0.2);
border-radius: 8px;
padding: 1.5rem;
margin-top: 1rem;
}
.bio-help p {
color: #a0aec0;
font-size: 0.9rem;
margin: 0 0 1rem 0;
line-height: 1.5;
}
.bio-help strong {
color: #667eea;
}
.bio-help ul {
margin: 0;
padding-left: 1.5rem;
}
.bio-help li {
color: #a0aec0;
font-size: 0.9rem;
margin-bottom: 0.5rem;
line-height: 1.4;
}
.placeholder-icon {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.3;
}
.placeholder-content p {
font-size: 1rem;
margin: 0;
line-height: 1.5;
}
/* BEAUTIFUL CARD-SPECIFIC STYLING FROM BACKUP */
.highlights-card {
background: linear-gradient(135deg, rgba(251, 191, 36, 0.1), rgba(245, 158, 11, 0.1));
border-color: rgba(251, 191, 36, 0.3);
}
.highlights-card:hover {
border-color: rgba(251, 191, 36, 0.5);
box-shadow: 0 20px 40px rgba(251, 191, 36, 0.2);
}
.about-card {
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1), rgba(118, 75, 162, 0.1));
border-color: rgba(102, 126, 234, 0.3);
}
.about-card:hover {
border-color: rgba(102, 126, 234, 0.5);
box-shadow: 0 20px 40px rgba(102, 126, 234, 0.2);
}
.influences-card {
background: linear-gradient(135deg, rgba(72, 187, 120, 0.1), rgba(56, 161, 105, 0.1));
border-color: rgba(72, 187, 120, 0.3);
}
.influences-card:hover {
border-color: rgba(72, 187, 120, 0.5);
box-shadow: 0 20px 40px rgba(72, 187, 120, 0.2);
}
.equipment-card {
background: linear-gradient(135deg, rgba(237, 137, 54, 0.1), rgba(221, 107, 32, 0.1));
border-color: rgba(237, 137, 54, 0.3);
}
.equipment-card:hover {
border-color: rgba(237, 137, 54, 0.5);
box-shadow: 0 20px 40px rgba(237, 137, 54, 0.2);
}
.achievements-card {
background: linear-gradient(135deg, rgba(245, 158, 11, 0.1), rgba(217, 119, 6, 0.1));
border-color: rgba(245, 158, 11, 0.3);
}
.achievements-card:hover {
border-color: rgba(245, 158, 11, 0.5);
box-shadow: 0 20px 40px rgba(245, 158, 11, 0.2);
}
.statement-card {
background: linear-gradient(135deg, rgba(168, 85, 247, 0.1), rgba(147, 51, 234, 0.1));
border-color: rgba(168, 85, 247, 0.3);
}
.statement-card:hover {
border-color: rgba(168, 85, 247, 0.5);
box-shadow: 0 20px 40px rgba(168, 85, 247, 0.2);
}
/* Responsive adjustments for profile cards */
@media (max-width: 1024px) {
.profile-cards-grid {
grid-template-columns: repeat(2, 1fr);
}
/* Reset row positioning for 2-column layout */
.profile-cards-grid .profile-card:nth-child(4),
.profile-cards-grid .profile-card:nth-child(5),
.profile-cards-grid .profile-card:nth-child(6) {
grid-row: auto;
}
}
@media (max-width: 768px) {
.profile-cards-grid {
grid-template-columns: 1fr !important;
gap: 1.5rem;
}
/* Reset all grid-row assignments on mobile - force single column */
.profile-cards-grid .profile-card {
grid-row: auto !important;
grid-column: 1 !important;
width: 100% !important;
max-width: 100% !important;
}
.profile-cards-grid .profile-card:nth-child(1),
.profile-cards-grid .profile-card:nth-child(2),
.profile-cards-grid .profile-card:nth-child(3),
.profile-cards-grid .profile-card:nth-child(4),
.profile-cards-grid .profile-card:nth-child(5),
.profile-cards-grid .profile-card:nth-child(6),
.profile-cards-grid .profile-card:nth-child(7),
.profile-cards-grid .profile-card:nth-child(8) {
grid-row: auto !important;
grid-column: 1 !important;
}
.profile-cards-container {
padding: 0 1rem;
}
.profile-cards-title {
font-size: 2rem;
}
.profile-card {
padding: 1.5rem;
min-height: auto;
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
.card-content {
min-height: 100px;
}
.card-header {
flex-wrap: wrap;
}
.card-title {
font-size: 1.2rem;
}
}
/* Profile Edit 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: 10000;
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;
}
.modal-body textarea {
width: 100%;
background: #3a3a3a;
border: 1px solid #4a4a4a;
border-radius: 10px;
padding: 1rem;
color: white;
font-size: 1rem;
line-height: 1.6;
resize: vertical;
min-height: 120px;
font-family: inherit;
}
.modal-body textarea:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
}
.read-only-content {
background: #3a3a3a;
border: 1px solid #4a4a4a;
border-radius: 10px;
padding: 1rem;
color: #e0e0e0;
font-size: 1rem;
line-height: 1.6;
min-height: 120px;
white-space: pre-wrap;
}
.modal-footer {
background: #3a3a3a;
padding: 1.5rem 2rem;
border-top: 1px solid #4a4a4a;
display: flex;
gap: 1rem;
justify-content: flex-end;
}
.btn-secondary {
background: rgba(255, 255, 255, 0.1);
color: white;
border: 2px solid rgba(255, 255, 255, 0.2);
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.2);
border-color: rgba(255, 255, 255, 0.3);
}
/* Animations */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Make cards look interactive */
.profile-card {
cursor: pointer;
position: relative;
}
.profile-card:hover {
transform: translateY(-5px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
}
.profile-card:hover::before {
opacity: 1;
}
.profile-card:active {
transform: translateY(-2px);
}
/* Card header layout */
.card-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
position: relative;
}
.card-icon {
font-size: 2rem;
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 12px;
background: rgba(255, 255, 255, 0.1);
flex-shrink: 0;
}
.card-title {
font-size: 1.3rem;
font-weight: 600;
color: white;
margin: 0;
flex: 1;
}
.card-edit-icon {
font-size: 1.2rem;
color: #a0a0a0;
opacity: 0.7;
transition: all 0.3s ease;
cursor: pointer;
flex-shrink: 0;
}
.profile-card:hover .card-edit-icon {
opacity: 1;
color: #667eea;
}
</style>
<!-- Clean Audio Player JavaScript -->
<script>
// Set user session variables for JavaScript
<?php
$isLoggedIn = isset($_SESSION['user_id']) && $_SESSION['user_id'] ? 'true' : 'false';
$isOwner = isset($_SESSION['user_id']) && $_SESSION['user_id'] && isset($artist_id) && $_SESSION['user_id'] == $artist_id ? 'true' : 'false';
?>
window.userIsLoggedIn = <?php echo $isLoggedIn; ?>;
window.userIsOwner = <?php echo $isOwner; ?>;
// Debug session variables
console.log('đ Session Debug:', {
userIsLoggedIn: window.userIsLoggedIn,
userIsOwner: window.userIsOwner,
phpIsLoggedIn: '<?php echo $isLoggedIn; ?>',
phpIsOwner: '<?php echo $isOwner; ?>',
sessionUserId: '<?php echo $_SESSION['user_id'] ?? 'null'; ?>',
artistId: '<?php echo $artist_id ?? 'null'; ?>'
});
// Test API endpoint
console.log('đ§Ē Testing API endpoint...');
fetch('/api_social.php', {
method: 'POST',
body: new FormData()
})
.then(response => response.text())
.then(data => {
console.log('â
API endpoint response:', data);
})
.catch(error => {
console.error('â API endpoint error:', error);
});
// Global player initialization check
function waitForGlobalPlayer(callback, maxAttempts = 20) {
if (window.globalPlayerReady && window.enhancedGlobalPlayer && typeof window.enhancedGlobalPlayer.playTrack === 'function') {
callback();
return;
}
if (maxAttempts > 0) {
setTimeout(() => waitForGlobalPlayer(callback, maxAttempts - 1), 250);
} else {
if (typeof window.showNotification === 'function') {
window.showNotification(notificationTranslations.audio_player_not_available, 'error');
}
}
}
// Enable play buttons when global player is ready
function enablePlayButtons() {
document.querySelectorAll('.overlay-play-btn.play-track-btn, .play-track-btn, .action-btn.play-btn, .variation-play-btn').forEach(btn => {
btn.classList.add('ready');
btn.disabled = false;
const icon = btn.querySelector('i');
if (icon && icon.className.includes('fa-spinner')) {
icon.className = 'fas fa-play';
}
});
}
// Update play button UI
function updatePlayButtonUI(trackId) {
// Reset all play buttons (supports both old and new card layouts)
document.querySelectorAll('.play-track-btn, .overlay-play-btn, .variation-play-btn').forEach(btn => {
btn.classList.remove('playing');
const icon = btn.querySelector('i');
if (icon) icon.className = 'fas fa-play';
});
// Update current track button
const currentButton = document.querySelector(`[data-track-id="${trackId}"] .play-track-btn`);
if (currentButton) {
currentButton.classList.add('playing');
const icon = currentButton.querySelector('i');
if (icon) icon.className = 'fas fa-pause';
}
}
// Track play count functionality
function recordTrackPlay(trackId) {
if (!trackId) {
console.warn('đĩ recordTrackPlay: No track ID provided');
return;
}
// 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_track_play.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ 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);
} else {
console.warn('đĩ Play count recording failed:', data.error);
}
})
.catch(error => {
console.warn('đĩ Play count error:', error);
});
} else {
console.log('đĩ Play count skipped (recently played):', trackId);
}
}
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', function() {
// Enable buttons immediately - global player will handle playback
enablePlayButtons();
// Enhanced play button functionality - single track playback without auto-advance
const playButtons = document.querySelectorAll('.play-track-btn, .variation-play-btn');
const artistId = <?= $artist_id ?>;
const artistName = '<?= htmlspecialchars($artist['username'] ?? 'Artist', ENT_QUOTES, 'UTF-8') ?>';
// Listen to global player events to update button states
const audioElement = document.getElementById('globalAudioElement');
if (audioElement) {
// When audio source changes, update all button states
audioElement.addEventListener('loadstart', function() {
// Clear all playing states when a new track loads
document.querySelectorAll('.play-track-btn, .variation-play-btn').forEach(btn => {
btn.classList.remove('playing');
const icon = btn.querySelector('i');
if (icon) icon.className = 'fas fa-play';
});
});
// When track starts playing, update the correct button
audioElement.addEventListener('play', function() {
const currentSrc = audioElement.src;
if (currentSrc) {
// Log the actual duration to debug
if (audioElement.duration) {
console.log('đĩ Track playing - Duration:', audioElement.duration, 'seconds');
}
// Find the button that matches this audio URL by comparing track IDs AND variation index
// Extract track ID and variation index from the playing audio URL
const getTrackIdFromUrl = (url) => {
if (!url) return null;
const match = url.match(/[?&]id=(\d+)/);
return match ? match[1] : null;
};
const getVariationIndexFromUrl = (url) => {
if (!url) return null;
const match = url.match(/[?&]variation(?:_index)?=(\d+)/);
return match ? match[1] : null;
};
const playingTrackId = getTrackIdFromUrl(currentSrc);
const playingVariationIndex = getVariationIndexFromUrl(currentSrc);
playButtons.forEach(btn => {
const btnTrackId = btn.getAttribute('data-track-id');
const btnVariationIndex = btn.getAttribute('data-variation-index');
const isVariationBtn = btn.classList.contains('variation-play-btn');
// For variation buttons, match both track ID and variation index
// For main track buttons, only match track ID when no variation is playing
let isMatch = false;
if (isVariationBtn) {
// Variation button: must match both track ID and variation index
isMatch = playingTrackId && btnTrackId && playingTrackId === btnTrackId &&
playingVariationIndex !== null && btnVariationIndex !== null &&
playingVariationIndex === btnVariationIndex;
} else {
// Main track button: match track ID, but only if no variation is playing
isMatch = playingTrackId && btnTrackId && playingTrackId === btnTrackId &&
playingVariationIndex === null;
}
if (isMatch) {
// This is the playing track - set it to playing
btn.classList.add('playing');
const icon = btn.querySelector('i');
if (icon) icon.className = 'fas fa-pause';
} else {
// Not this track - clear playing state
btn.classList.remove('playing');
const icon = btn.querySelector('i');
if (icon) icon.className = 'fas fa-play';
}
});
}
});
// Listen for loadedmetadata to check actual duration
audioElement.addEventListener('loadedmetadata', function() {
console.log('đĩ Audio metadata loaded - Duration:', audioElement.duration, 'seconds');
if (audioElement.duration && audioElement.duration < 60) {
console.warn('â ī¸ WARNING: Audio file appears to be a preview (duration:', audioElement.duration, 'seconds)');
}
});
// Add timeupdate listener to detect if playback stops prematurely
let lastReportedTime = 0;
audioElement.addEventListener('timeupdate', function() {
const currentTime = audioElement.currentTime;
// If time jumps back or stops unexpectedly, log it
if (currentTime < lastReportedTime - 0.5) {
console.warn('â ī¸ Playback time jumped backwards from', lastReportedTime, 'to', currentTime);
}
// If we're at 44 seconds and duration is longer, check if it's about to stop
if (currentTime >= 43 && currentTime <= 45 && audioElement.duration > 60) {
console.warn('â ī¸ At 44 seconds - Duration is', audioElement.duration, '- checking if playback will continue');
}
lastReportedTime = currentTime;
});
// When track pauses, update button state
audioElement.addEventListener('pause', function() {
const currentSrc = audioElement.src;
if (currentSrc) {
// Extract track ID and variation index from the playing audio URL
const getTrackIdFromUrl = (url) => {
if (!url) return null;
const match = url.match(/[?&]id=(\d+)/);
return match ? match[1] : null;
};
const getVariationIndexFromUrl = (url) => {
if (!url) return null;
const match = url.match(/[?&]variation(?:_index)?=(\d+)/);
return match ? match[1] : null;
};
const playingTrackId = getTrackIdFromUrl(currentSrc);
const playingVariationIndex = getVariationIndexFromUrl(currentSrc);
playButtons.forEach(btn => {
const btnTrackId = btn.getAttribute('data-track-id');
const btnVariationIndex = btn.getAttribute('data-variation-index');
const isVariationBtn = btn.classList.contains('variation-play-btn');
// Match using same logic as play event
let isMatch = false;
if (isVariationBtn) {
isMatch = playingTrackId && btnTrackId && playingTrackId === btnTrackId &&
playingVariationIndex !== null && btnVariationIndex !== null &&
playingVariationIndex === btnVariationIndex;
} else {
isMatch = playingTrackId && btnTrackId && playingTrackId === btnTrackId &&
playingVariationIndex === null;
}
if (isMatch) {
// This is the paused track - update icon to play
const icon = btn.querySelector('i');
if (icon) icon.className = 'fas fa-play';
}
});
}
});
// When track ends, clear all button states
audioElement.addEventListener('ended', function() {
console.log('đĩ Track ended - clearing all button states');
document.querySelectorAll('.play-track-btn, .variation-play-btn').forEach(btn => {
btn.classList.remove('playing');
const icon = btn.querySelector('i');
if (icon) icon.className = 'fas fa-play';
});
});
}
playButtons.forEach(button => {
// Mark as listener attached to prevent duplicate listeners
button.setAttribute('data-listener-attached', 'true');
button.addEventListener('click', async function(e) {
e.preventDefault();
e.stopPropagation();
const audioUrl = this.getAttribute('data-audio-url');
const title = this.getAttribute('data-title');
const artist = this.getAttribute('data-artist');
const trackId = this.getAttribute('data-track-id');
console.log('=== Button Click Debug ===');
console.log('audioUrl:', audioUrl);
console.log('title:', title);
console.log('artist:', artist);
console.log('trackId:', trackId);
console.log('========================');
if (!audioUrl) {
showNotification(notificationTranslations.audio_not_available, 'error');
return;
}
// Use global player
if (typeof window.enhancedGlobalPlayer !== 'undefined') {
// Check if this button is already in playing state and the same track is playing
const isCurrentlyPlaying = this.classList.contains('playing');
const globalPlayer = window.enhancedGlobalPlayer;
const audioElement = document.getElementById('globalAudioElement');
const variationIndex = this.getAttribute('data-variation-index');
// Check if the same track/variation is currently loaded (playing or paused)
// Compare by track ID AND variation index (for variation buttons)
let isSameTrack = false;
if (audioElement && audioElement.src) {
const getTrackIdFromUrl = (url) => {
if (!url) return null;
const match = url.match(/[?&]id=(\d+)/);
return match ? match[1] : null;
};
const getVariationIndexFromUrl = (url) => {
if (!url) return null;
const match = url.match(/[?&]variation(?:_index)?=(\d+)/);
return match ? match[1] : null;
};
const currentTrackId = getTrackIdFromUrl(audioElement.src);
const currentVariationIndex = getVariationIndexFromUrl(audioElement.src);
// For variation buttons, must match both track ID and variation index
// For main buttons, match track ID only when no variation is playing
if (variationIndex !== null) {
isSameTrack = currentTrackId && currentTrackId === trackId &&
currentVariationIndex !== null && currentVariationIndex === variationIndex;
} else {
isSameTrack = currentTrackId && currentTrackId === trackId && currentVariationIndex === null;
}
}
// If same track is loaded (playing or paused), toggle pause/resume instead of restarting
if (isSameTrack) {
console.log('đĩ Same track is loaded - toggling pause/resume (paused:', audioElement.paused, ')');
globalPlayer.togglePlayPause();
// Update button state after a short delay to allow toggle to complete
setTimeout(() => {
if (audioElement.paused) {
this.classList.remove('playing');
const icon = this.querySelector('i');
if (icon) icon.className = 'fas fa-play';
} else {
this.classList.add('playing');
const icon = this.querySelector('i');
if (icon) icon.className = 'fas fa-pause';
}
}, 50);
return;
}
// Clear other playing states
document.querySelectorAll('.play-track-btn, .variation-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';
// Load artist playlist for auto-advance through artist's tracks
console.log('đĩ Loading artist playlist for auto-advance...');
try {
// Load the artist's playlist
const response = await fetch(`/api/get_artist_tracks.php?artist_id=${artistId}&type=completed&_t=${Date.now()}`);
const data = await response.json();
if (data.success && data.tracks && data.tracks.length > 0) {
// Process tracks to match the format expected by global player
// Use signed_audio_url which respects variation selection (set by track owner)
// This ensures autoplay uses the same variation as the selected track
const formattedTracks = data.tracks.map(track => ({
id: track.id,
audio_url: track.signed_audio_url || track.audio_url, // Use signed URL that respects variation selection
title: track.title,
artist_name: track.artist_name || artist,
duration: track.duration, // Main track duration
user_id: track.user_id || artistId
}));
// Load playlist into global player
if (window.enhancedGlobalPlayer && window.enhancedGlobalPlayer.loadArtistPlaylist) {
window.enhancedGlobalPlayer.loadArtistPlaylist(formattedTracks, artistName, false);
console.log('đĩ Artist playlist loaded with', formattedTracks.length, 'tracks - auto-advance enabled');
} else {
// Fallback: manually set playlist
window.enhancedGlobalPlayer.currentPlaylist = formattedTracks;
window.enhancedGlobalPlayer.currentPlaylistType = 'artist_' + artistId;
window.enhancedGlobalPlayer.autoPlayEnabled = true; // Enable auto-advance
console.log('đĩ Artist playlist set manually - auto-advance enabled');
}
} else {
console.warn('đĩ No tracks found for artist playlist');
}
} catch (error) {
console.error('Error loading artist playlist:', error);
// Continue anyway - at least play the track
}
// Now play the track - without playlist, it won't auto-advance
console.log('Calling global player with:', {audioUrl, title, artist, trackId});
console.log('đĩ Audio URL being used:', audioUrl);
// Ensure audio URL is absolute if it's relative (same as track.php)
let finalAudioUrl = audioUrl;
if (audioUrl && !audioUrl.startsWith('http') && !audioUrl.startsWith('//')) {
// Make relative URL absolute
if (audioUrl.startsWith('/')) {
finalAudioUrl = window.location.origin + audioUrl;
} else {
finalAudioUrl = window.location.origin + '/' + audioUrl;
}
console.log('đĩ Converted relative URL to absolute:', finalAudioUrl);
}
// Verify we're using the full track URL, not a preview
// If the URL contains 'preview' or is suspiciously short, warn
if (finalAudioUrl.toLowerCase().includes('preview') || finalAudioUrl.toLowerCase().includes('30s') || finalAudioUrl.toLowerCase().includes('44s')) {
console.error('â ī¸ WARNING: Audio URL appears to be a preview:', finalAudioUrl);
}
// Verify audio URL is not empty
if (!finalAudioUrl || finalAudioUrl.trim() === '') {
console.error('â Audio URL is empty!');
showNotification('â ' + notificationTranslations.audio_file_not_available, 'error');
return;
}
// Call global player with trackId and artistId for proper navigation
const success = window.enhancedGlobalPlayer.playTrack(finalAudioUrl, title, artist, trackId, artistId);
if (success) {
// Record play count
if (trackId) {
recordTrackPlay(trackId);
}
showNotification('đĩ ' + notificationTranslations.now_playing.replace(':title', title), 'success');
} else {
console.error('â Global player failed to play track');
showNotification('â ' + notificationTranslations.failed_play_track, 'error');
// Reset button state on failure
this.classList.remove('playing');
const icon = this.querySelector('i');
if (icon) icon.className = 'fas fa-play';
}
} else {
showNotification(notificationTranslations.player_not_available, 'error');
this.classList.remove('playing');
const icon = this.querySelector('i');
if (icon) icon.className = 'fas fa-play';
}
});
});
// Image upload functionality
const profileImageUpload = document.getElementById('profileImageUpload');
const coverImageUpload = document.getElementById('coverImageUpload');
if (profileImageUpload) {
profileImageUpload.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
console.log('đĩ Profile image selected:', file.name);
uploadProfileImage(file);
}
});
}
if (coverImageUpload) {
coverImageUpload.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
console.log('đĩ Cover image selected:', file.name);
uploadCoverImage(file);
}
});
}
});
// Profile image upload function
function uploadProfileImage(file) {
console.log('đĩ Starting profile image upload...', file.name);
const formData = new FormData();
formData.append('profile_image', file);
formData.append('action', 'upload_profile_image');
// Show loading state
const profileContainer = document.querySelector('.profile-image-container');
const originalContent = profileContainer.innerHTML;
profileContainer.innerHTML = '<div class="profile-placeholder"><i class="fas fa-spinner fa-spin"></i></div>';
fetch('/api_social.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
console.log('đĩ Profile image upload successful, reloading page...');
location.reload();
} else {
console.error('đĩ Profile image upload failed:', data.message);
alert('Error uploading profile image: ' + (data.message || 'Unknown error'));
profileContainer.innerHTML = originalContent;
}
})
.catch(error => {
console.error('đĩ Profile image upload error:', error);
alert('Error uploading profile image. Please try again.');
profileContainer.innerHTML = originalContent;
});
}
// Cover image upload function
function uploadCoverImage(file) {
console.log('đĩ Starting cover image upload...', file.name);
const formData = new FormData();
formData.append('cover_image', file);
formData.append('action', 'upload_cover_image');
// Show loading state
const coverContainer = document.querySelector('.cover-image-container');
const originalContent = coverContainer.innerHTML;
coverContainer.innerHTML = '<div class="cover-placeholder"><i class="fas fa-spinner fa-spin"></i></div>';
fetch('/api_social.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
console.log('đĩ Cover image upload successful, reloading page...');
location.reload();
} else {
console.error('đĩ Cover image upload failed:', data.message);
alert('Error uploading cover image: ' + (data.message || 'Unknown error'));
coverContainer.innerHTML = originalContent;
}
})
.catch(error => {
console.error('đĩ Cover image upload error:', error);
alert('Error uploading cover image. Please try again.');
coverContainer.innerHTML = originalContent;
});
}
// Cover Image Repositioning Functions
let isRepositioning = false;
let originalPosition = 'center 50%';
let currentPosition = 'center 50%';
let isDragging = false;
let startY = 0;
function toggleCoverReposition() {
const container = document.querySelector('.cover-image-container');
const coverImage = document.querySelector('.cover-image');
const repositionBtn = document.querySelector('.reposition-btn');
const saveBtn = document.querySelector('.save-position-btn');
const cancelBtn = document.querySelector('.cancel-position-btn');
if (!container || !coverImage) {
console.error('đĩ Cover image elements not found');
return;
}
if (!isRepositioning) {
// Start repositioning mode
isRepositioning = true;
container.classList.add('repositioning');
repositionBtn.style.display = 'none';
saveBtn.style.display = 'flex';
cancelBtn.style.display = 'flex';
// Get current position from style attribute or database value
const dbPosition = '<?= htmlspecialchars($artist['cover_position'] ?? 'center center') ?>';
const stylePosition = coverImage.style.objectPosition || getComputedStyle(coverImage).objectPosition;
// Normalize position format - ensure it's in "center X%" format
originalPosition = normalizePosition(stylePosition || dbPosition);
currentPosition = originalPosition;
// Set the initial position on the image
coverImage.style.objectPosition = currentPosition;
// Ensure image is loaded before allowing drag
if (!coverImage.complete || coverImage.naturalHeight === 0) {
coverImage.addEventListener('load', function onImageLoad() {
coverImage.removeEventListener('load', onImageLoad);
console.log('đĩ Cover image loaded, repositioning ready');
});
}
// Add drag event listeners
coverImage.addEventListener('mousedown', startDrag);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', endDrag);
// Add touch event listeners for mobile
coverImage.addEventListener('touchstart', startDragTouch, { passive: false });
document.addEventListener('touchmove', dragTouch, { passive: false });
document.addEventListener('touchend', endDrag);
console.log('đĩ Cover image repositioning mode activated', { originalPosition, currentPosition });
}
}
// Helper function to normalize position format - ALWAYS returns "center X%" format
function normalizePosition(position) {
if (!position || position === 'initial' || position === 'inherit') {
return 'center 50%';
}
// If already in "center X%" format, return as is
if (position.includes('center') && position.includes('%')) {
return position;
}
// Convert "center center" to "center 50%"
if (position === 'center center' || position.trim() === 'center') {
return 'center 50%';
}
// Try to parse other formats
const parts = position.split(' ');
if (parts.length >= 2) {
const yPart = parts[1];
if (yPart.includes('%')) {
// Already has percentage, just ensure center is there
return `center ${yPart}`;
} else if (yPart === 'center' || yPart === 'top' || yPart === 'bottom') {
const yPercent = yPart === 'top' ? '0%' : (yPart === 'bottom' ? '100%' : '50%');
return `center ${yPercent}`;
}
}
// If we can't parse it, default to center 50%
return 'center 50%';
}
function startDrag(e) {
if (!isRepositioning) return;
isDragging = true;
startY = e.clientY;
e.preventDefault();
}
function startDragTouch(e) {
if (!isRepositioning) return;
isDragging = true;
startY = e.touches[0].clientY;
e.preventDefault();
}
function drag(e) {
if (!isDragging || !isRepositioning) return;
e.preventDefault();
const coverImage = document.querySelector('.cover-image');
const container = document.querySelector('.cover-image-container');
if (!coverImage || !container) return;
const deltaY = e.clientY - startY;
const containerHeight = container.offsetHeight;
// Parse current position (same approach as profile image)
let posY = 50; // Default center
const currentPos = coverImage.style.objectPosition || currentPosition;
if (currentPos) {
const parts = currentPos.split(' ');
if (parts.length >= 2) {
const yPart = parts[1];
if (yPart.includes('%')) {
posY = parseFloat(yPart);
} else if (yPart === 'center') {
posY = 50;
} else if (yPart === 'top') {
posY = 0;
} else if (yPart === 'bottom') {
posY = 100;
}
}
}
// Calculate new position (inverted because dragging moves the visible area)
// Use container height for calculation (same as profile uses container width)
const newY = Math.max(0, Math.min(100, posY - (deltaY / containerHeight) * 50));
currentPosition = `center ${newY.toFixed(1)}%`;
coverImage.style.objectPosition = currentPosition;
// Reset start position for smooth dragging (key difference!)
startY = e.clientY;
}
function dragTouch(e) {
if (!isDragging || !isRepositioning) return;
e.preventDefault();
const coverImage = document.querySelector('.cover-image');
const container = document.querySelector('.cover-image-container');
if (!coverImage || !container || !e.touches || e.touches.length === 0) return;
const deltaY = e.touches[0].clientY - startY;
const containerHeight = container.offsetHeight;
// Parse current position from currentPosition variable (same approach as profile image)
let posY = 50; // Default center
const parts = currentPosition.split(' ');
if (parts.length >= 2) {
const yPart = parts[1];
if (yPart.includes('%')) {
posY = parseFloat(yPart);
} else if (yPart === 'center') {
posY = 50;
} else if (yPart === 'top') {
posY = 0;
} else if (yPart === 'bottom') {
posY = 100;
}
}
// Calculate new position (inverted because dragging moves the visible area)
const newY = Math.max(0, Math.min(100, posY - (deltaY / containerHeight) * 50));
currentPosition = `center ${newY.toFixed(1)}%`;
coverImage.style.objectPosition = currentPosition;
// Reset start position for smooth dragging (key difference!)
startY = e.touches[0].clientY;
e.preventDefault();
}
function endDrag() {
isDragging = false;
}
function saveCoverPosition() {
const coverImage = document.querySelector('.cover-image');
if (!coverImage) {
console.error('đĩ Cover image not found');
showNotification('Error: Cover image not found', 'error');
return;
}
// Use currentPosition directly (same approach as profile image)
// It's already in the correct format from drag operations
const positionToSave = currentPosition;
console.log('đĩ Saving cover position:', positionToSave);
const formData = new FormData();
formData.append('action', 'save_cover_position');
formData.append('position', positionToSave);
formData.append('artist_id', <?= $artist['id'] ?>);
// Show loading state
const saveBtn = document.querySelector('.save-position-btn');
const originalBtnContent = saveBtn.innerHTML;
saveBtn.disabled = true;
saveBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> <span>Saving...</span>';
fetch('/api_social.php', {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.text().then(text => {
try {
return JSON.parse(text);
} catch (e) {
console.error('đĩ Invalid JSON response:', text);
throw new Error('Invalid response from server');
}
});
})
.then(data => {
saveBtn.disabled = false;
saveBtn.innerHTML = originalBtnContent;
if (data && data.success) {
console.log('đĩ Cover position saved successfully:', positionToSave);
// Update the stored position
currentPosition = positionToSave;
originalPosition = positionToSave;
// Update the image style to persist
coverImage.style.objectPosition = positionToSave;
showNotification(notificationTranslations.cover_position_saved || 'Cover position saved successfully', 'success');
exitRepositionMode();
} else {
console.error('đĩ Failed to save cover position:', data?.message || 'Unknown error');
showNotification((notificationTranslations.failed_save_position || 'Failed to save position') + ': ' + (data?.message || notificationTranslations.unknown_error || 'Unknown error'), 'error');
}
})
.catch(error => {
saveBtn.disabled = false;
saveBtn.innerHTML = originalBtnContent;
console.error('đĩ Error saving cover position:', error);
showNotification(notificationTranslations.error_saving_position || 'Error saving position. Please try again.', 'error');
});
}
function cancelCoverReposition() {
const coverImage = document.querySelector('.cover-image');
if (coverImage) {
// Restore original position (normalize it first)
const normalizedOriginal = normalizePosition(originalPosition);
coverImage.style.objectPosition = normalizedOriginal;
currentPosition = normalizedOriginal;
console.log('đĩ Cover position restored to:', normalizedOriginal);
}
exitRepositionMode();
}
function exitRepositionMode() {
const container = document.querySelector('.cover-image-container');
const coverImage = document.querySelector('.cover-image');
const repositionBtn = document.querySelector('.reposition-btn');
const saveBtn = document.querySelector('.save-position-btn');
const cancelBtn = document.querySelector('.cancel-position-btn');
if (!container || !coverImage) return;
isRepositioning = false;
isDragging = false;
container.classList.remove('repositioning');
if (repositionBtn) repositionBtn.style.display = 'flex';
if (saveBtn) {
saveBtn.style.display = 'none';
saveBtn.disabled = false;
}
if (cancelBtn) cancelBtn.style.display = 'none';
// Remove event listeners (use same function references)
if (coverImage) {
coverImage.removeEventListener('mousedown', startDrag);
coverImage.removeEventListener('touchstart', startDragTouch);
}
document.removeEventListener('mousemove', drag);
document.removeEventListener('mouseup', endDrag);
document.removeEventListener('touchmove', dragTouch);
document.removeEventListener('touchend', endDrag);
console.log('đĩ Cover image repositioning mode deactivated');
}
// Profile Image Repositioning Functions
let isProfileRepositioning = false;
let originalProfilePosition = 'center center';
let currentProfilePosition = 'center center';
let isProfileDragging = false;
let profileStartX = 0;
let profileStartY = 0;
function toggleProfileReposition(event) {
if (event) event.stopPropagation();
const container = document.querySelector('.profile-image-container');
const profileImage = document.querySelector('.profile-image');
const repositionBtn = document.querySelector('.profile-reposition-btn');
const saveBtn = document.querySelector('.save-profile-position-btn');
const cancelBtn = document.querySelector('.cancel-profile-position-btn');
if (!profileImage || !container) return;
if (!isProfileRepositioning) {
// Start repositioning mode
isProfileRepositioning = true;
container.classList.add('repositioning');
repositionBtn.style.display = 'none';
saveBtn.style.display = 'flex';
cancelBtn.style.display = 'flex';
// Store original position
originalProfilePosition = profileImage.style.objectPosition || '<?= htmlspecialchars($artist['profile_position'] ?? 'center center') ?>';
currentProfilePosition = originalProfilePosition;
// Add drag listeners
profileImage.addEventListener('mousedown', startProfileDrag);
document.addEventListener('mousemove', dragProfile);
document.addEventListener('mouseup', endProfileDrag);
profileImage.addEventListener('touchstart', startProfileDragTouch);
document.addEventListener('touchmove', dragProfileTouch);
document.addEventListener('touchend', endProfileDrag);
console.log('đĩ Profile image repositioning mode activated');
}
}
function startProfileDrag(e) {
if (!isProfileRepositioning) return;
isProfileDragging = true;
profileStartX = e.clientX;
profileStartY = e.clientY;
e.preventDefault();
}
function startProfileDragTouch(e) {
if (!isProfileRepositioning) return;
isProfileDragging = true;
profileStartX = e.touches[0].clientX;
profileStartY = e.touches[0].clientY;
e.preventDefault();
}
function dragProfile(e) {
if (!isProfileDragging || !isProfileRepositioning) return;
const deltaX = e.clientX - profileStartX;
const deltaY = e.clientY - profileStartY;
const profileImage = document.querySelector('.profile-image');
const container = document.querySelector('.profile-image-container');
const containerSize = container.offsetWidth;
// Parse current position
let [posX, posY] = currentProfilePosition.split(' ').map(p => {
if (p.includes('%')) return parseFloat(p);
if (p === 'center') return 50;
if (p === 'left' || p === 'top') return 0;
if (p === 'right' || p === 'bottom') return 100;
return 50;
});
// Calculate new position (inverted because dragging moves the visible area)
const newX = Math.max(0, Math.min(100, posX - (deltaX / containerSize) * 50));
const newY = Math.max(0, Math.min(100, posY - (deltaY / containerSize) * 50));
currentProfilePosition = `${newX.toFixed(1)}% ${newY.toFixed(1)}%`;
profileImage.style.objectPosition = currentProfilePosition;
// Reset start position for smooth dragging
profileStartX = e.clientX;
profileStartY = e.clientY;
}
function dragProfileTouch(e) {
if (!isProfileDragging || !isProfileRepositioning) return;
const deltaX = e.touches[0].clientX - profileStartX;
const deltaY = e.touches[0].clientY - profileStartY;
const profileImage = document.querySelector('.profile-image');
const container = document.querySelector('.profile-image-container');
const containerSize = container.offsetWidth;
// Parse current position
let [posX, posY] = currentProfilePosition.split(' ').map(p => {
if (p.includes('%')) return parseFloat(p);
if (p === 'center') return 50;
if (p === 'left' || p === 'top') return 0;
if (p === 'right' || p === 'bottom') return 100;
return 50;
});
// Calculate new position
const newX = Math.max(0, Math.min(100, posX - (deltaX / containerSize) * 50));
const newY = Math.max(0, Math.min(100, posY - (deltaY / containerSize) * 50));
currentProfilePosition = `${newX.toFixed(1)}% ${newY.toFixed(1)}%`;
profileImage.style.objectPosition = currentProfilePosition;
// Reset start position for smooth dragging
profileStartX = e.touches[0].clientX;
profileStartY = e.touches[0].clientY;
e.preventDefault();
}
function endProfileDrag() {
isProfileDragging = false;
}
function saveProfilePosition(event) {
if (event) event.stopPropagation();
console.log('đĩ Saving profile position:', currentProfilePosition);
const formData = new FormData();
formData.append('action', 'save_profile_position');
formData.append('position', currentProfilePosition);
formData.append('artist_id', <?= $artist_id ?>);
fetch('/api_social.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
console.log('đĩ Profile position saved successfully');
showNotification(notificationTranslations.profile_position_saved, 'success');
exitProfileRepositionMode();
} else {
console.error('đĩ Failed to save profile position:', data.message);
showNotification(notificationTranslations.failed_save_position + ': ' + (data.message || notificationTranslations.unknown_error), 'error');
}
})
.catch(error => {
console.error('đĩ Error saving profile position:', error);
showNotification(notificationTranslations.error_saving_position, 'error');
});
}
function cancelProfileReposition(event) {
if (event) event.stopPropagation();
// Restore original position
const profileImage = document.querySelector('.profile-image');
if (profileImage) {
profileImage.style.objectPosition = originalProfilePosition;
}
exitProfileRepositionMode();
}
function exitProfileRepositionMode() {
const container = document.querySelector('.profile-image-container');
const profileImage = document.querySelector('.profile-image');
const repositionBtn = document.querySelector('.profile-reposition-btn');
const saveBtn = document.querySelector('.save-profile-position-btn');
const cancelBtn = document.querySelector('.cancel-profile-position-btn');
isProfileRepositioning = false;
if (container) container.classList.remove('repositioning');
if (repositionBtn) repositionBtn.style.display = 'flex';
if (saveBtn) saveBtn.style.display = 'none';
if (cancelBtn) cancelBtn.style.display = 'none';
// Remove event listeners
if (profileImage) {
profileImage.removeEventListener('mousedown', startProfileDrag);
profileImage.removeEventListener('touchstart', startProfileDragTouch);
}
document.removeEventListener('mousemove', dragProfile);
document.removeEventListener('mouseup', endProfileDrag);
document.removeEventListener('touchmove', dragProfileTouch);
document.removeEventListener('touchend', endProfileDrag);
console.log('đĩ Profile image repositioning mode deactivated');
}
// Translation strings for notifications
const notificationTranslations = <?= json_encode([
'added_to_cart' => t('notification.added_to_cart'),
'failed_add_cart' => t('notification.failed_add_cart'),
'please_log_in_follow' => t('notification.please_log_in_follow'),
'following_artist' => t('notification.following_artist'),
'unfollowed_successfully' => t('notification.unfollowed_successfully'),
'failed_update_follow' => t('notification.failed_update_follow'),
'please_log_in_friends' => t('notification.please_log_in_friends'),
'please_log_in_messages' => t('notification.please_log_in_messages'),
'failed_play_track' => t('notification.failed_play_track'),
'audio_not_available' => t('notification.audio_not_available'),
'audio_player_not_available' => t('notification.audio_player_not_available'),
'audio_file_not_available' => t('notification.audio_file_not_available'),
'now_playing' => t('notification.now_playing'),
'player_not_available' => t('notification.player_not_available'),
'cover_position_saved' => t('notification.cover_position_saved'),
'profile_position_saved' => t('notification.profile_position_saved'),
'failed_save_position' => t('notification.failed_save_position'),
'error_saving_position' => t('notification.error_saving_position'),
'failed_submit_track_rating' => t('notification.failed_submit_track_rating'),
'failed_submit_rating' => t('notification.failed_submit_rating'),
'failed_update_profile' => t('notification.failed_update_profile'),
'failed_update_profile_retry' => t('notification.failed_update_profile_retry'),
'failed_load_artist_radio' => t('notification.failed_load_artist_radio'),
'added' => t('notification.added'),
'loading' => t('notification.loading'),
'please_try_again' => t('notification.please_try_again'),
'unknown_error' => t('notification.unknown_error'),
'network_error_like' => t('notification.network_error_like'),
'failed_update_friend_status' => t('notification.failed_update_friend_status'),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
// Simple notification function if not already defined
function showNotification(message, type = 'info') {
if (typeof window.showNotification === 'function') {
window.showNotification(message, type);
} else {
alert(message);
}
}
// Show message when PayPal.Me is not set up
function showPayPalMeNotSetupMessage() {
<?php if (isset($_SESSION['user_id']) && $_SESSION['user_id'] == $artist['id']): ?>
// If it's the artist's own profile, show message with option to go to settings
const message = <?= json_encode(t('artist_profile.paypal_me_not_setup_own') ?? 'You haven\'t set up your PayPal.Me link yet. You can add it in Profile Settings in the PayPal.Me section.') ?>;
if (confirm(message + '\n\n' + <?= json_encode(t('artist_profile.open_settings') ?? 'Would you like to go to Profile Settings?') ?>)) {
window.location.href = '/profile_settings.php';
}
<?php else: ?>
// If it's someone else's profile, just show info message
const message = <?= json_encode(t('artist_profile.paypal_me_not_setup_message') ?? 'This artist hasn\'t set up their PayPal.Me link yet.') ?>;
if (typeof window.showNotification === 'function') {
window.showNotification(message, 'info');
} else {
alert(message);
}
<?php endif; ?>
}
function openLeaderboardModal() {
const modal = document.getElementById('leaderboardModal');
if (!modal) return;
modal.classList.add('active');
modal.setAttribute('aria-hidden', 'false');
}
function closeLeaderboardModal() {
const modal = document.getElementById('leaderboardModal');
if (!modal) return;
modal.classList.remove('active');
modal.setAttribute('aria-hidden', 'true');
}
</script>
<!-- Artist Hero Section -->
<div class="artist-hero">
<div class="hero-content">
<!-- Cover Image -->
<div class="cover-image-container">
<?php if ($artist['cover_image']): ?>
<img src="<?= htmlspecialchars($artist['cover_image']) ?>"
alt="<?= htmlspecialchars($artist['username']) ?> Cover"
class="cover-image"
style="object-position: <?= htmlspecialchars($artist['cover_position'] ?? 'center center') ?>;">
<?php else: ?>
<div class="cover-placeholder">
<div class="cover-placeholder-pattern">
<div class="cover-placeholder-waveform"></div>
<div class="cover-placeholder-music-notes">
<i class="fas fa-music"></i>
<i class="fas fa-headphones"></i>
<i class="fas fa-sliders-h"></i>
</div>
</div>
<div class="cover-placeholder-gradient-overlay"></div>
</div>
<?php endif; ?>
<?php if (isset($_SESSION['user_id']) && $_SESSION['user_id'] == $artist['id']): ?>
<div class="cover-upload-overlay">
<div class="upload-label" onclick="document.getElementById('coverImageUpload').click();">
<i class="fas fa-camera"></i>
<span><?= t('artist_profile.change_cover') ?></span>
</div>
<input type="file" id="coverImageUpload" accept="image/*" style="display: none;">
</div>
<!-- Cover Image Reposition Controls -->
<div class="cover-reposition-controls">
<button class="reposition-btn" onclick="toggleCoverReposition()">
<i class="fas fa-arrows-alt-v"></i>
<span><?= t('artist_profile.reposition') ?></span>
</button>
<button class="save-position-btn" onclick="saveCoverPosition()" style="display: none;">
<i class="fas fa-save"></i>
<span><?= t('artist_profile.save_position') ?></span>
</button>
<button class="cancel-position-btn" onclick="cancelCoverReposition()" style="display: none;">
<i class="fas fa-times"></i>
<span><?= t('artist_profile.cancel') ?></span>
</button>
</div>
<?php endif; ?>
</div>
<!-- Artist Info Section -->
<div class="artist-info-section">
<div class="profile-image-container">
<?php
// Format and check profile image
$profileImg = formatProfileImage($artist['profile_image'], $artist['username']);
$artistInitial = strtoupper(substr($artist['username'], 0, 1));
?>
<?php if ($profileImg): ?>
<img src="<?= htmlspecialchars($profileImg) ?>"
alt="<?= htmlspecialchars($artist['username']) ?>"
class="profile-image"
style="object-position: <?= htmlspecialchars($artist['profile_position'] ?? 'center center') ?>;"
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
<div class="profile-placeholder" style="display:none;">
<span class="profile-initial"><?= htmlspecialchars($artistInitial) ?></span>
</div>
<?php else: ?>
<div class="profile-placeholder">
<span class="profile-initial"><?= htmlspecialchars($artistInitial) ?></span>
</div>
<?php endif; ?>
<?php if (isset($_SESSION['user_id']) && $_SESSION['user_id'] == $artist['id']): ?>
<div class="profile-upload-overlay">
<div class="upload-label" onclick="document.getElementById('profileImageUpload').click();">
<i class="fas fa-camera"></i>
<span><?= t('artist_profile.change_photo') ?></span>
</div>
<input type="file" id="profileImageUpload" accept="image/*" style="display: none;">
</div>
<?php if ($profileImg): ?>
<!-- Profile Image Reposition Controls -->
<div class="profile-reposition-controls">
<button class="profile-reposition-btn" onclick="toggleProfileReposition(event)">
<i class="fas fa-arrows-alt"></i>
<span><?= t('artist_profile.reposition') ?></span>
</button>
<button class="save-profile-position-btn" onclick="saveProfilePosition(event)" style="display: none;">
<i class="fas fa-check"></i>
</button>
<button class="cancel-profile-position-btn" onclick="cancelProfileReposition(event)" style="display: none;">
<i class="fas fa-times"></i>
</button>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
<div class="artist-details">
<h1 class="artist-name"><?= htmlspecialchars($artist['username']) ?></h1>
<div style="display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem;">
<div class="artist-badge"><?= $artist['badge'] ?></div>
<!-- PayPal Donation Button -->
<?php if (!empty($artist['paypal_me_username'])): ?>
<a href="https://paypal.me/<?= htmlspecialchars($artist['paypal_me_username'], ENT_QUOTES) ?>"
target="_blank"
rel="noopener noreferrer"
class="donation-button" style="margin: 0;">
<i class="fab fa-paypal"></i>
<span><?= t('artist_profile.donate') ?? 'Donate via PayPal' ?></span>
</a>
<?php else: ?>
<button type="button"
class="donation-button"
style="margin: 0; cursor: not-allowed; opacity: 0.7;"
onclick="showPayPalMeNotSetupMessage()"
title="<?= t('artist_profile.paypal_me_not_setup') ?? 'PayPal.Me not set up' ?>">
<i class="fab fa-paypal"></i>
<span><?= t('artist_profile.donate') ?? 'Donate via PayPal' ?></span>
</button>
<?php endif; ?>
</div>
<?php if ($artist['location']): ?>
<div class="artist-location">đ <?= htmlspecialchars($artist['location']) ?></div>
<?php endif; ?>
<!-- Genre Tags -->
<?php
$genres = json_decode($artist['genres'] ?? '[]', true);
if (!empty($genres)):
?>
<div class="genre-tags">
<?php foreach ($genres as $genre): ?>
<span class="genre-tag"><?= htmlspecialchars($genre) ?></span>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php
$hasVoteScore = ($artist['rating_count'] ?? 0) > 0;
$leaderboardRank = $artist['leaderboard_rank'] ?? null;
$leaderboardTotal = $artist['leaderboard_total'] ?? 0;
$topArtist = $artist['top_artist'] ?? null;
$voteScorePercent = $hasVoteScore ? max(0, min(100, ($artist['vote_score'] / 10) * 100)) : 0;
?>
<div class="artist-stats primary-stats">
<div class="stat-item">
<span class="stat-number"><?= number_format($artist['friends_count'] ?? 0) ?></span>
<span class="stat-label"><?= t('artist_profile.friends') ?></span>
</div>
<div class="stat-item">
<span class="stat-number"><?= number_format($artist['followers_count']) ?></span>
<span class="stat-label"><?= ($artist['followers_count'] == 1) ? t('artist_profile.follower') : t('artist_profile.followers') ?></span>
</div>
<div class="stat-item">
<span class="stat-number"><?= number_format($artist['following_count']) ?></span>
<span class="stat-label"><?= t('artist_profile.following') ?></span>
</div>
<div class="stat-item">
<span class="stat-number"><?= number_format($artist['total_plays']) ?></span>
<span class="stat-label"><?= t('artist_profile.total_play') ?></span>
</div>
<div class="stat-item">
<span class="stat-number"><?= number_format($artist['completed_tracks']) ?></span>
<span class="stat-label"><?= t('artist_profile.tracks') ?></span>
</div>
<div class="stat-item">
<span class="stat-number"><?= number_format($artist['playlists_count'] ?? 0) ?></span>
<span class="stat-label"><?= (($artist['playlists_count'] ?? 0) == 1) ? t('artist_profile.playlist') : t('artist_profile.playlists') ?></span>
</div>
</div>
<?php
// Display Artist Ranking Card (similar to track ranking card)
// Show ranking card if artist has at least one complete track
if (($artist['completed_tracks'] ?? 0) > 0 && !empty($artistRankings) && isset($artistRankings['overall'])):
// Get user's current artist rating if logged in
$user_artist_rating = null;
if (isset($_SESSION['user_id'])) {
$user_rating_stmt = $pdo->prepare("SELECT rating FROM artist_ratings WHERE artist_id = ? AND user_id = ?");
$user_rating_stmt->execute([$artist_id, $_SESSION['user_id']]);
$user_rating_result = $user_rating_stmt->fetch();
$user_artist_rating = $user_rating_result ? $user_rating_result['rating'] : null;
}
?>
<?php
$rank = (int)($artistRankings['overall'] ?? 0);
$is_top_ten = $rank > 0 && $rank <= 10;
$badge_config = [
1 => ['color' => '#fbbf24', 'gradient' => 'linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%)', 'icon' => 'fa-crown', 'text' => t('artist_profile.platform_leaderboard_you'), 'border' => '2px solid #fbbf24', 'bg' => 'linear-gradient(135deg, rgba(251, 191, 36, 0.15) 0%, rgba(251, 191, 36, 0.08) 100%)'],
2 => ['color' => '#c0c0c0', 'gradient' => 'linear-gradient(135deg, #c0c0c0 0%, #a8a8a8 100%)', 'icon' => 'fa-medal', 'text' => t('artist_profile.rank_badge_2nd'), 'border' => '2px solid #c0c0c0', 'bg' => 'linear-gradient(135deg, rgba(192, 192, 192, 0.15) 0%, rgba(168, 168, 168, 0.08) 100%)'],
3 => ['color' => '#cd7f32', 'gradient' => 'linear-gradient(135deg, #cd7f32 0%, #b87333 100%)', 'icon' => 'fa-medal', 'text' => t('artist_profile.rank_badge_3rd'), 'border' => '2px solid #cd7f32', 'bg' => 'linear-gradient(135deg, rgba(205, 127, 50, 0.15) 0%, rgba(184, 115, 51, 0.08) 100%)'],
];
$badge = $badge_config[$rank] ?? null;
?>
<div class="card-track-ranking" style="margin-top: 1.5rem; margin-bottom: 1.5rem; cursor: pointer; <?= $badge ? $badge['border'] . '; background: ' . $badge['bg'] . ';' : '' ?>" onclick="openArtistRankingModal(<?= $artist_id ?>, <?= htmlspecialchars(json_encode($artistRankings), ENT_QUOTES) ?>, <?= htmlspecialchars(json_encode([
'id' => $artist_id,
'username' => $artist['username'],
'total_plays' => $artist['total_plays'] ?? 0,
'total_likes' => $artist['total_likes'] ?? 0,
'average_rating' => $artist['average_rating'] ?? 0,
'rating_count' => $artist['rating_count'] ?? 0
]), ENT_QUOTES) ?>)">
<?php if ($badge): ?>
<div style="background: <?= $badge['gradient'] ?>; color: <?= $rank == 1 ? '#000' : '#fff' ?>; padding: 0.75rem 1rem; margin: -1rem -1rem 1rem -1rem; border-radius: 8px 8px 0 0; text-align: center; font-weight: 700; font-size: 0.9rem; box-shadow: 0 4px 12px rgba(<?= $rank == 1 ? '251, 191, 36' : ($rank == 2 ? '192, 192, 192' : '205, 127, 50') ?>, 0.4); position: relative; overflow: hidden;">
<div style="position: absolute; top: -50%; right: -50%; width: 200%; height: 200%; background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%); pointer-events: none;"></div>
<div style="position: relative; z-index: 1;">
<i class="fas <?= $badge['icon'] ?>" style="margin-right: 0.5rem; font-size: 1.1rem;"></i>
<?= $badge['text'] ?>
<?php if ($rank == 1): ?>
<div style="font-size: 0.75rem; font-weight: 500; margin-top: 0.25rem; opacity: 0.9;">
<?= str_replace(':votes', number_format($artist['rating_count'] ?? 0), t('artist_profile.platform_leaderboard_you_desc')) ?>
</div>
<?php else: ?>
<div style="font-size: 0.75rem; font-weight: 500; margin-top: 0.25rem; opacity: 0.9;">
<?= str_replace([':rank', ':total'], [$rank, $artistRankings['total_artists'] ?? 0], t('artist_profile.rank_badge_desc')) ?>
</div>
<?php endif; ?>
</div>
</div>
<?php elseif ($is_top_ten): ?>
<div style="background: linear-gradient(135deg, rgba(102, 126, 234, 0.2) 0%, rgba(102, 126, 234, 0.1) 100%); color: #667eea; padding: 0.5rem 1rem; margin: -1rem -1rem 1rem -1rem; border-radius: 8px 8px 0 0; text-align: center; font-weight: 600; font-size: 0.85rem; border-bottom: 1px solid rgba(102, 126, 234, 0.3);">
<i class="fas fa-star" style="margin-right: 0.5rem;"></i>
<?= str_replace(':rank', $rank, t('artist_profile.rank_badge_top_ten')) ?>
</div>
<?php endif; ?>
<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($artistRankings['overall']) ?></span>
<span class="ranking-out-of-compact">/ <?= number_format($artistRankings['total_artists']) ?></span>
</div>
<div class="ranking-percentile-compact">
<span class="percentile-badge-compact"><?= str_replace(':percent', $artistRankings['overall_percentile'], t('artist_profile.top_percentile')) ?></span>
</div>
<div class="ranking-details-compact">
<div class="ranking-detail-item">
<i class="fas fa-headphones-alt"></i>
<span>#<?= number_format($artistRankings['plays']) ?></span>
</div>
<div class="ranking-detail-item">
<i class="fas fa-heart"></i>
<span>#<?= number_format($artistRankings['likes']) ?></span>
</div>
<?php if (($artist['rating_count'] ?? 0) >= 3 && isset($artistRankings['rating'])): ?>
<div class="ranking-detail-item">
<i class="fas fa-star"></i>
<span>#<?= number_format($artistRankings['rating']) ?></span>
</div>
<?php endif; ?>
</div>
</div>
<!-- Artist Rating Section (instead of voting buttons) -->
<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"
onclick="event.stopPropagation(); showRatingModal();"
title="<?= t('artist_profile.rate_artist') ?>">
<i class="fas fa-star"></i>
</button>
<div class="vote-count-card" style="font-size: 0.9rem;">
<?= number_format($artist['rating_count'] ?? 0) ?>
</div>
<div style="font-size: 0.75rem; color: #a0aec0; text-align: center;">
<?= number_format($artist['average_rating'] ?? 0, 1) ?>/10
</div>
</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; ?>
<?php
// Old platform leaderboard card removed - now using comprehensive artist ranking card instead
// The comprehensive ranking (plays + likes + ratings) is more accurate than rating-only ranking
?>
<div class="artist-actions">
<?php if (isset($_SESSION['user_id']) && $_SESSION['user_id'] == $artist['id']): ?>
<button class="action-btn radio-btn" onclick="playArtistRadio()">
<i class="fas fa-play"></i>
<span><?= t('artist_profile.play_artist_radio') ?></span>
</button>
<!-- Edit Profile button removed temporarily -->
<?php else: ?>
<button class="action-btn radio-btn" onclick="playArtistRadio()">
<i class="fas fa-play"></i>
<span><?= t('artist_profile.play_artist_radio') ?></span>
</button>
<button class="action-btn follow-btn <?= $is_following ? 'following' : '' ?>"
onclick="toggleFollow(<?= $artist['id'] ?>, this)">
<i class="fas fa-<?= $is_following ? 'heart' : 'user-plus' ?>"></i>
<span><?= $is_following ? t('artist_profile.following') : t('artist_profile.follow') ?></span>
</button>
<button class="action-btn friend-btn <?= $friend_status === 'friends' ? 'friend' : ($friend_status === 'pending' ? 'pending' : '') ?>"
onclick="toggleFriend(<?= $artist['id'] ?>, this)"
data-artist-id="<?= $artist['id'] ?>"
data-current-status="<?= $friend_status ?>"
<?= $friend_status === 'pending' ? 'disabled' : '' ?>>
<?php if ($friend_status === 'friends'): ?>
<i class="fas fa-user-times"></i>
<span>Unfriend</span>
<?php elseif ($friend_status === 'pending'): ?>
<i class="fas fa-clock"></i>
<span>Pending</span>
<?php else: ?>
<i class="fas fa-user-plus"></i>
<span><?= t('artist_profile.add_friend') ?></span>
<?php endif; ?>
</button>
<button class="action-btn message-btn" onclick="openMessageModal(<?= $artist['id'] ?>, '<?= htmlspecialchars($artist['username'], ENT_QUOTES, 'UTF-8') ?>')">
<i class="fas fa-envelope"></i>
<span><?= t('artist_profile.message') ?></span>
</button>
<?php endif; ?>
<button class="action-btn share-btn" onclick="shareArtist()" data-custom-url="<?= htmlspecialchars($artist['custom_url'] ?? '', ENT_QUOTES, 'UTF-8') ?>" data-artist-id="<?= $artist['id'] ?>">
<i class="fas fa-share"></i>
<span><?= t('artist_profile.share') ?></span>
</button>
</div>
</div>
</div>
</div>
</div>
<?php if (!empty($artist_events)): ?>
<div class="enhanced-sidebar-section artist-events-wrapper">
<div class="section-header-modern">
<div class="header-content">
<div class="header-icon">đĢ</div>
<div class="header-text">
<h3 class="section-title-modern"><?= t('artist_profile.events_section_title') ?></h3>
<p class="section-subtitle"><?= t('artist_profile.events_section_subtitle') ?></p>
</div>
</div>
</div>
<div class="artist-events-grid">
<?php foreach ($artist_events as $event):
$dateRange = ssp_format_event_date_range($event['start_date'] ?? null, $event['end_date'] ?? null);
$sold = (int)$event['tickets_sold'];
$capacity = $event['max_attendees'] ?? null;
$remaining = $capacity ? max($capacity - $sold, 0) : null;
?>
<article class="artist-event-card" onclick="openArtistEventModal(<?= $event['id'] ?>)">
<div class="event-cover" style="background-image: url('<?= htmlspecialchars($event['cover_image'] ?: $event['banner_image'] ?: '') ?>');">
<span class="event-type"><?= ucwords(str_replace('_', ' ', $event['event_type'])) ?></span>
</div>
<div class="event-card-body">
<h4><?= htmlspecialchars($event['title']) ?></h4>
<?php if ($dateRange): ?>
<p class="event-date-line"><i class="fas fa-calendar"></i> <?= htmlspecialchars($dateRange) ?></p>
<?php endif; ?>
<?php if (!empty($event['venue_name']) || !empty($event['location'])): ?>
<p><i class="fas fa-map-marker-alt"></i> <?= htmlspecialchars($event['venue_name'] ?: $event['location']) ?></p>
<?php endif; ?>
<div class="event-tickets-meta">
<div class="event-ticket-chip sold">
<small><?= t('artist_profile.tickets_sold') ?></small>
<strong><?= number_format($sold) ?><?= $capacity ? ' / ' . number_format($capacity) : '' ?></strong>
</div>
<div class="event-ticket-chip remaining">
<small><?= t('artist_profile.tickets_remaining') ?></small>
<strong>
<?php if ($capacity): ?>
<?= $remaining === 0 ? t('artist_profile.sold_out') : number_format($remaining) ?>
<?php else: ?>
â
<?php endif; ?>
</strong>
</div>
</div>
</div>
<div class="event-card-footer">
<button type="button" onclick="event.stopPropagation(); openArtistEventModal(<?= $event['id'] ?>)">
<i class="fas fa-eye"></i>
<?= t('artist_profile.view_event') ?>
</button>
</div>
</article>
<?php endforeach; ?>
</div>
</div>
<?php elseif (isset($_SESSION['user_id']) && $_SESSION['user_id'] == $artist['id']): ?>
<div class="enhanced-sidebar-section artist-events-wrapper">
<div class="empty-state">
<div class="empty-icon">đ</div>
<h4><?= t('artist_profile.no_events') ?></h4>
<p><?= t('artist_profile.no_events_desc_self') ?></p>
<a class="leaderboard-link" href="/events.php"><?= t('artist_profile.create_event_cta') ?></a>
</div>
</div>
<?php endif; ?>
<?php if (!empty($artist['leaderboard_top_artists'])): ?>
<div id="leaderboardModal" class="leaderboard-modal" aria-hidden="true">
<div class="leaderboard-modal-content">
<button class="leaderboard-modal-close" onclick="closeLeaderboardModal()" aria-label="Close leaderboard modal">×</button>
<h3 style="margin-top:0; color:#fff;"><?= t('artist_profile.platform_leaderboard_modal_title') ?></h3>
<p style="color:#cbd5f5;"><?= t('artist_profile.platform_leaderboard_modal_desc') ?></p>
<div class="leaderboard-map">
<div class="leaderboard-map-grid">
<?php foreach ($artist['leaderboard_top_artists'] as $index => $leader): ?>
<div class="leaderboard-map-card">
<h4>#<?= $index + 1 ?> <?= htmlspecialchars($leader['artist_name']) ?></h4>
<span><?= number_format($leader['avg_rating'], 1) ?>/10 ¡ <?= number_format($leader['vote_count']) . ' ' . t('artist_profile.votes') ?></span>
<?php if (!empty($leader['location'])): ?>
<span>đ <?= htmlspecialchars($leader['location']) ?></span>
<?php endif; ?>
<a href="/artist_profile.php?id=<?= $leader['artist_id'] ?>" class="leaderboard-link" style="margin-top:0.5rem;">
<?= t('artist_profile.platform_leaderboard_view_top') ?>
</a>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
<?php endif; ?>
<!-- Artist Profile Cards Section -->
<div class="profile-cards-section">
<div class="profile-cards-container">
<div class="profile-cards-header">
<h2 class="profile-cards-title"><?= t('artist_profile.artist_profile') ?></h2>
<p class="profile-cards-subtitle"><?= t('artist_profile.complete_profile_desc') ?></p>
</div>
<div class="profile-cards-grid">
<!-- Average Rating - Interactive voting card -->
<div class="profile-card rating-card" onclick="showRatingModal();">
<div class="card-header">
<div class="card-icon">â</div>
<h3 class="card-title"><?= t('artist_profile.average_rating') ?></h3>
<div class="card-edit-icon">đŗī¸</div>
</div>
<div class="card-content">
<div class="rating-display">
<div class="stars-container">
<?php
$rating = isset($artist['average_rating']) ? (float)$artist['average_rating'] : 0;
$rating_count = isset($artist['rating_count']) ? (int)$artist['rating_count'] : 0;
$filledStars = (int)floor($rating + 0.0001);
for ($i = 1; $i <= 10; $i++) {
$isFilled = $i <= $filledStars;
$iconClass = $isFilled ? 'fas' : 'far';
echo "<i class=\"{$iconClass} fa-star\"></i>";
}
?>
</div>
<div class="rating-text"><?= number_format($rating, 1) ?>/10</div>
<div class="rating-count">(<?= $rating_count ?> <?= t('artist_profile.votes') ?>)</div>
</div>
<div class="rating-action">
<button class="btn-rate" onclick="event.stopPropagation(); showRatingModal();">
<?= t('artist_profile.rate_artist') ?>
</button>
</div>
</div>
</div>
<!-- Artist Highlights - Always show with placeholder if empty -->
<div class="profile-card highlights-card" onclick="editProfileCard('highlights', '<?= htmlspecialchars($artist['artist_highlights'] ?? '', ENT_QUOTES) ?>')">
<div class="card-header">
<div class="card-icon">â</div>
<h3 class="card-title"><?= t('artist_profile.highlights') ?></h3>
<div class="card-edit-icon">âī¸</div>
</div>
<div class="card-content">
<?php
$highlights = json_decode($artist['artist_highlights'] ?? '[]', true);
if (!empty($highlights)): ?>
<div class="highlights-list">
<?php foreach ($highlights as $highlight): ?>
<div class="highlight-item">
<i class="fas fa-star"></i>
<span><?= htmlspecialchars($highlight) ?></span>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="placeholder-content">
<div class="placeholder-icon">â</div>
<p><?= t('artist_profile.no_highlights') ?></p>
</div>
<?php endif; ?>
</div>
</div>
<!-- About Section - Always show -->
<div class="profile-card about-card" onclick="console.log('đ¯ About card clicked!'); editProfileCard('bio', '<?= htmlspecialchars($artist['bio'] ?? '', ENT_QUOTES) ?>')">
<div class="card-header">
<div class="card-icon">đ</div>
<h3 class="card-title">About</h3>
<div class="card-edit-icon">âī¸</div>
</div>
<div class="card-content">
<?php if ($artist['bio']): ?>
<div class="bio-content">
<?= nl2br(htmlspecialchars($artist['bio'])) ?>
</div>
<?php endif; ?>
<?php if (!$artist['bio']): ?>
<div class="placeholder-content">
<div class="placeholder-icon">đ</div>
<p><?= t('artist_profile.no_bio') ?></p>
</div>
<?php endif; ?>
</div>
</div>
<!-- Influences - Always show -->
<div class="profile-card influences-card" onclick="console.log('đ¯ Influences card clicked!'); console.log('đ¯ Calling editProfileCard for influences'); editProfileCard('influences', '<?= htmlspecialchars($artist['influences'] ?? '', ENT_QUOTES) ?>')">
<div class="card-header">
<div class="card-icon">đ</div>
<h3 class="card-title"><?= t('artist_profile.influences') ?></h3>
<div class="card-edit-icon">âī¸</div>
</div>
<div class="card-content">
<?php if ($artist['influences']): ?>
<div class="influences-content">
<?= nl2br(htmlspecialchars($artist['influences'])) ?>
</div>
<?php else: ?>
<div class="placeholder-content">
<div class="placeholder-icon">đ</div>
<p><?= t('artist_profile.no_influences') ?></p>
</div>
<?php endif; ?>
</div>
</div>
<!-- Equipment - Always show -->
<div class="profile-card equipment-card" onclick="editProfileCard('equipment', '<?= htmlspecialchars($artist['equipment'] ?? '', ENT_QUOTES) ?>')">
<div class="card-header">
<div class="card-icon">đī¸</div>
<h3 class="card-title"><?= t('artist_profile.equipment') ?></h3>
<div class="card-edit-icon">âī¸</div>
</div>
<div class="card-content">
<?php if ($artist['equipment']): ?>
<div class="equipment-content">
<?= nl2br(htmlspecialchars($artist['equipment'])) ?>
</div>
<?php else: ?>
<div class="placeholder-content">
<div class="placeholder-icon">đī¸</div>
<p><?= t('artist_profile.no_equipment') ?></p>
</div>
<?php endif; ?>
</div>
</div>
<!-- Achievements - Always show -->
<div class="profile-card achievements-card" onclick="editProfileCard('achievements', '<?= htmlspecialchars($artist['achievements'] ?? '', ENT_QUOTES) ?>')">
<div class="card-header">
<div class="card-icon">đ</div>
<h3 class="card-title"><?= t('artist_profile.achievements') ?></h3>
<div class="card-edit-icon">âī¸</div>
</div>
<div class="card-content">
<?php
$achievements = json_decode($artist['achievements'] ?? '[]', true);
if (!empty($achievements)): ?>
<div class="achievements-list">
<?php foreach ($achievements as $achievement): ?>
<div class="achievement-item">
<i class="fas fa-trophy"></i>
<span><?= htmlspecialchars($achievement) ?></span>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="placeholder-content">
<div class="placeholder-icon">đ</div>
<p><?= t('artist_profile.no_achievements') ?></p>
</div>
<?php endif; ?>
</div>
</div>
<!-- Artist Statement - Always show -->
<div class="profile-card statement-card" onclick="console.log('đ¯ Artist Statement card clicked!'); console.log('đ¯ Calling editProfileCard for artist_statement'); editProfileCard('artist_statement', '<?= htmlspecialchars($artist['artist_statement'] ?? '', ENT_QUOTES) ?>')">
<div class="card-header">
<div class="card-icon">đ</div>
<h3 class="card-title"><?= t('artist_profile.artist_statement') ?></h3>
<div class="card-edit-icon">âī¸</div>
</div>
<div class="card-content">
<?php if ($artist['artist_statement']): ?>
<div class="statement-content">
<em>"<?= htmlspecialchars($artist['artist_statement']) ?>"</em>
</div>
<?php else: ?>
<div class="placeholder-content">
<div class="placeholder-icon">đ</div>
<p><?= t('artist_profile.no_statement') ?></p>
</div>
<?php endif; ?>
</div>
</div>
<!-- Engagement Rate - Interactive engagement card -->
<div class="profile-card engagement-card" onclick="showEngagementModal()">
<div class="card-header">
<div class="card-icon">đ</div>
<h3 class="card-title"><?= t('artist_profile.engagement_rate') ?></h3>
<div class="card-edit-icon">đ</div>
</div>
<div class="card-content">
<div class="engagement-display">
<?php
$monthly_listeners = $artist['monthly_listeners'] ?? 0;
$total_track_plays = $artist['total_track_plays'] ?? 0;
$followers = $artist['followers_count'] ?? 0;
// Calculate engagement rate based on monthly listeners vs followers
$engagement_rate = $followers > 0 ? min(100, round(($monthly_listeners / $followers) * 100)) : 0;
$engagement_label = $engagement_rate >= 70 ? t('artist_profile.high_engagement') : ($engagement_rate >= 40 ? t('artist_profile.medium_engagement') : t('artist_profile.low_engagement'));
?>
<div class="engagement-bar">
<div class="engagement-fill" style="width: <?= $engagement_rate ?>%"></div>
</div>
<div class="engagement-text"><?= $engagement_rate ?>%</div>
<div class="engagement-label"><?= $engagement_label ?></div>
</div>
<div class="engagement-stats">
<div class="stat-item">
<span class="stat-number"><?= number_format($monthly_listeners) ?></span>
<span class="stat-label"><?= t('artist_profile.monthly_listeners') ?></span>
</div>
<div class="stat-item">
<span class="stat-number"><?= number_format($total_track_plays) ?></span>
<span class="stat-label"><?= t('artist_profile.track_plays') ?></span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- CrateList Section - Public Crates -->
<div class="cratelist-section" id="cratelistSection">
<div class="cratelist-container">
<div class="cratelist-header">
<div class="header-content">
<div class="header-icon">đĻ</div>
<div class="header-text">
<h2 class="cratelist-title"><?= t('artist_profile.cratelist') ?></h2>
<p class="cratelist-subtitle"><?= t('artist_profile.cratelist_desc') ?></p>
</div>
</div>
</div>
<div class="cratelist-grid" id="cratelistGrid">
<!-- Crates will be loaded dynamically -->
<div class="cratelist-loading">
<i class="fas fa-spinner fa-spin"></i>
<span><?= t('library.crates.loading') ?></span>
</div>
</div>
</div>
</div>
<!-- Music Store Section -->
<div class="music-marketplace">
<div class="marketplace-container">
<div class="marketplace-header">
<h2 class="marketplace-title">đĩ <?= t('artist_profile.music_store') ?></h2>
<p class="marketplace-subtitle"><?= t('artist_profile.music_store_desc') ?></p>
</div>
<!-- Music Grid -->
<div class="music-grid">
<?php if (empty($tracks)): ?>
<div class="empty-store">
<div class="empty-icon">đĩ</div>
<h3><?= t('artist_profile.no_music_available') ?></h3>
<p><?= t('artist_profile.no_music_desc') ?></p>
<button class="btn-primary" onclick="openMessageModal(<?= $artist['id'] ?>, '<?= htmlspecialchars($artist['username'], ENT_QUOTES, 'UTF-8') ?>')"><?= t('artist_profile.contact_artist') ?></button>
</div>
<?php else: ?>
<?php
// DEBUG: Let's see what we actually have
echo "<!-- DEBUG: Artist username: " . htmlspecialchars($artist['username']) . " -->";
echo "<!-- DEBUG: Artist ID: " . $artist['id'] . " -->";
echo "<!-- DEBUG: Number of tracks: " . count($tracks) . " -->";
?>
<?php foreach ($tracks as $track): ?>
<?php
// DEBUG: Track info
echo "<!-- DEBUG TRACK: ID=" . $track['id'] . ", Title=" . htmlspecialchars($track['title']) . ", Artist=" . htmlspecialchars($track['artist_name'] ?? 'NULL') . " -->";
// Calculate rankings for this track
$trackRankings = calculateTrackRankings($pdo, $track);
$status = $track['status'];
$hasAudio = !empty($track['audio_url']);
$isPlayable = $status === 'complete' && $hasAudio;
// Use actual pricing from database, default to $1.99 if price is 0, null, or empty
$trackPrice = $track['price'] ?? 0;
$price = (!empty($trackPrice) && floatval($trackPrice) > 0) ? $trackPrice : '1.99';
// Debug: Log the price for troubleshooting
error_log("Track {$track['id']} - Original price: " . var_export($track['price'], true) . ", Final price: $price");
$originalPrice = '14.99';
$isOnSale = false; // No sales for now
$discount = 0;
// Use default genre and mood (can be extracted from metadata later)
$genre = 'Electronic';
$mood = 'Energetic';
// Try to extract genre/mood from metadata if available
if (!empty($track['metadata'])) {
$metadata = json_decode($track['metadata'], true);
if ($metadata) {
if (!empty($metadata['genre'])) $genre = $metadata['genre'];
if (!empty($metadata['mood'])) $mood = $metadata['mood'];
}
}
$displayTitle = $track['title'];
if (empty($displayTitle)) {
// Generate title from prompt if available
if (!empty($track['prompt'])) {
$displayTitle = substr($track['prompt'], 0, 50);
if (strlen($track['prompt']) > 50) {
$displayTitle .= '...';
}
} else {
$displayTitle = 'Untitled Track';
}
}
// Handle track image - same logic as library.php and community_fixed.php
$imageUrl = $track['image_url'] ?? null;
// Reject external URLs
if (!empty($imageUrl) && (strpos($imageUrl, 'http://') === 0 || strpos($imageUrl, 'https://') === 0)) {
$imageUrl = null;
}
// Normalize image URL format
if ($imageUrl && !preg_match('/^https?:\/\//', $imageUrl)) {
if (!str_starts_with($imageUrl, '/')) {
$imageUrl = '/' . ltrim($imageUrl, '/');
}
}
// If still empty, try metadata
if (empty($imageUrl) || $imageUrl === 'null' || $imageUrl === 'NULL') {
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'];
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'];
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
if (empty($imageUrl)) {
$imageUrl = '/assets/images/default-track.jpg';
}
}
$trackSignedUrl = getSignedAudioUrl($track['id']);
?>
<div class="music-card" data-track-id="<?= $track['id'] ?>" data-genre="<?= strtolower($genre) ?>" data-price="<?= $price ?>" data-duration="<?= $track['duration'] ?? 0 ?>" data-audio-url="<?= htmlspecialchars($trackSignedUrl) ?>" data-title="<?= htmlspecialchars($displayTitle) ?>">
<!-- Artwork with overlay -->
<div class="music-artwork">
<?php if ($imageUrl && $imageUrl !== '/assets/images/default-track.jpg'): ?>
<img src="<?= htmlspecialchars($imageUrl) ?>" alt="<?= htmlspecialchars($displayTitle) ?>" class="track-cover-image" loading="lazy">
<?php else: ?>
<div class="artwork-placeholder">
<span><?= substr(htmlspecialchars($displayTitle), 0, 1) ?></span>
</div>
<?php endif; ?>
<?php if ($isOnSale): ?>
<div class="sale-badge">-<?= $discount ?>%</div>
<?php endif; ?>
<div class="track-duration-badge">
<?= ($track['preferred_duration'] ?? $track['duration']) ? gmdate('i:s', ($track['preferred_duration'] ?? $track['duration'])) : '0:00' ?>
</div>
<div class="artwork-overlay">
<button class="overlay-play-btn play-track-btn"
data-audio-url="<?= htmlspecialchars($trackSignedUrl) ?>"
data-title="<?= htmlspecialchars($displayTitle) ?>"
data-artist="<?= htmlspecialchars($artist['username']) ?>"
data-track-id="<?= $track['id'] ?>">
<i class="fas fa-play"></i>
</button>
</div>
</div>
<!-- Track info -->
<div class="music-info">
<h3 class="music-title">
<a href="/track.php?id=<?= $track['id'] ?>" class="track-title-link" title="<?= htmlspecialchars($displayTitle) ?>">
<?= htmlspecialchars($displayTitle) ?>
</a>
<?php if ($price > 0): ?>
<span class="for-sale-chip">
<i class="fas fa-tag"></i>
$<?= $price ?>
</span>
<?php endif; ?>
</h3>
<div class="music-meta">
<a href="/community_fixed.php?genre=<?= urlencode($genre) ?>" class="genre-tag"><?= $genre ?></a>
<?php if ($mood && $mood !== $genre && strtolower($mood) !== 'neutral'): ?>
<a href="/community_fixed.php?genre=<?= urlencode($mood) ?>" class="mood-tag"><?= $mood ?></a>
<?php endif; ?>
</div>
<!-- Track Ranking Display with Integrated Voting -->
<?php if ($status === 'complete' && !empty($trackRankings) && isset($trackRankings['overall'])): ?>
<div class="card-track-ranking"
style="cursor: pointer; position: relative;"
onclick="openTrackRankingModal(<?= $track['id'] ?>, <?= htmlspecialchars(json_encode($trackRankings), ENT_QUOTES) ?>, <?= htmlspecialchars(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
]), ENT_QUOTES) ?>)">
<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; ?>
</div>
<!-- Stats bar -->
<div class="card-footer">
<div class="track-stats">
<span class="stat-item" title="<?= t('artist_profile.plays') ?>">
<i class="fas fa-headphones-alt"></i>
<?= number_format($track['play_count']) ?>
</span>
<button class="stat-item like-toggle <?= !empty($track['user_liked']) ? 'liked' : '' ?>"
onclick="toggleTrackLike(<?= $track['id'] ?>, this)"
title="<?= t('artist_profile.like') ?>">
<i class="fas fa-heart"></i>
<span class="like-count"><?= number_format($track['like_count']) ?></span>
</button>
<button class="stat-item comment-btn"
onclick="showComments(<?= $track['id'] ?>)"
title="<?= t('artist_profile.comments') ?>">
<i class="fas fa-comment"></i>
<span><?= number_format($track['comment_count'] ?? 0) ?></span>
</button>
<button class="stat-item share-track-btn"
onclick="shareTrackFromCard(<?= $track['id'] ?>, '<?= htmlspecialchars($displayTitle, ENT_QUOTES) ?>', '<?= htmlspecialchars($artist['username'], ENT_QUOTES) ?>')"
title="<?= t('artist_profile.share_track') ?>">
<i class="fas fa-share"></i>
<span><?= number_format($track['share_count'] ?? 0) ?></span>
</button>
<?php if (isset($_SESSION['user_id'])): ?>
<button class="stat-item 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; ?>
<span class="stat-item" onclick="showTrackRatingModal(<?= $track['id'] ?>, '<?= htmlspecialchars($displayTitle) ?>')" title="Rate this track" style="cursor: pointer;">
<i class="fas fa-star" style="color: <?= ($track['rating_count'] ?? 0) > 0 ? '#fbbf24' : '#6b7280' ?>;"></i>
<?php if (($track['rating_count'] ?? 0) > 0): ?>
<?= number_format($track['average_rating'] ?? 0, 1) ?>
<?php endif; ?>
</span>
<?php if (!empty($track['variations'])): ?>
<button class="stat-item variations-btn"
onclick="showVariations(<?= $track['id'] ?>, this)"
title="<?= count($track['variations']) ?> <?= t('community.variations') ?? 'Variations' ?>">
<i class="fas fa-layer-group"></i>
<span><?= count($track['variations']) ?></span>
</button>
<?php endif; ?>
</div>
</div>
<!-- Purchase section -->
<div class="card-purchase">
<div class="price-display">
<?php if ($isOnSale): ?>
<div class="original-price">$<?= number_format($originalPrice, 2) ?></div>
<?php endif; ?>
<div class="current-price">$<?= $price ?></div>
</div>
<?php $userPurchased = !empty($track['user_purchased']); ?>
<?php if ($userPurchased): ?>
<button class="add-to-cart-btn" disabled>
<i class="fas fa-check"></i>
<?= t('artist_profile.purchased') ?? 'Purchased' ?>
</button>
<?php else: ?>
<button class="add-to-cart-btn" onclick="addToCart(<?= $track['id'] ?>, '<?= htmlspecialchars($displayTitle) ?>', <?= $price ?>, this)">
<i class="fas fa-cart-plus"></i>
<?= t('artist_profile.add_to_cart') ?>
</button>
<?php endif; ?>
<?php $isWishlisted = !empty($track['is_in_wishlist']); ?>
<button class="wishlist-btn <?= $isWishlisted ? 'active' : '' ?>"
onclick="toggleWishlist(<?= $track['id'] ?>, this)"
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') ?? 'Audio Variations' ?> (<?= count($track['variations']) ?>)</span>
</div>
<div class="variations-grid">
<?php foreach ($track['variations'] as $var):
$variationIndex = isset($var['variation_index']) ? (int)$var['variation_index'] : null;
$variationDisplayTitle = $displayTitle . ' - ' . (t('community.variation') ?? 'Variation') . ' ' . (($variationIndex !== null) ? ($variationIndex + 1) : 1);
$sessionId = session_id();
$variationAudioUrl = getSignedAudioUrl($track['id'], $variationIndex, null, $current_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($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; ?>
<?php endif; ?>
</div>
<!-- Load More Section -->
<div class="load-more-section">
<button class="load-more-btn" onclick="loadMoreTracks()">
<i class="fas fa-plus"></i>
<?= t('artist_profile.load_more_tracks') ?>
</button>
</div>
</div>
</div>
<style>
/* CrateList Section Styles */
.cratelist-section {
background: linear-gradient(180deg, #0f0f0f 0%, #1a1a1a 100%);
padding: 4rem 0;
margin-bottom: 0;
}
.cratelist-container {
max-width: 120rem;
margin: 0 auto;
padding: 0 2rem;
}
.cratelist-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 3rem;
}
.cratelist-header .header-content {
display: flex;
align-items: center;
gap: 1.5rem;
}
.cratelist-header .header-icon {
font-size: 3rem;
filter: drop-shadow(0 4px 8px rgba(102, 126, 234, 0.3));
}
.cratelist-header .header-text {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.cratelist-title {
font-size: 2.5rem;
font-weight: 700;
color: white;
margin: 0;
background: linear-gradient(135deg, #667eea, #764ba2);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.cratelist-subtitle {
font-size: 1.1rem;
color: #a0a0a0;
margin: 0;
}
.cratelist-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 2rem;
}
.cratelist-loading {
grid-column: 1 / -1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 4rem;
color: #a0aec0;
font-size: 1.2rem;
gap: 1rem;
}
.cratelist-loading i {
font-size: 2.5rem;
color: #667eea;
}
.cratelist-empty {
grid-column: 1 / -1;
text-align: center;
padding: 4rem 2rem;
background: rgba(255, 255, 255, 0.03);
border-radius: 20px;
border: 1px dashed rgba(255, 255, 255, 0.1);
}
.cratelist-empty .empty-icon {
font-size: 4rem;
opacity: 0.4;
margin-bottom: 1.5rem;
}
.cratelist-empty h4 {
font-size: 1.5rem;
color: white;
margin: 0 0 0.5rem 0;
}
.cratelist-empty p {
font-size: 1.1rem;
color: #a0aec0;
margin: 0;
}
/* Individual Crate Card */
.crate-card {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(102, 126, 234, 0.2);
border-radius: 20px;
overflow: hidden;
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
cursor: pointer;
}
.crate-card:hover {
transform: translateY(-8px);
border-color: rgba(102, 126, 234, 0.5);
box-shadow: 0 20px 40px rgba(102, 126, 234, 0.15), 0 0 0 1px rgba(102, 126, 234, 0.3);
}
.crate-card-header {
position: relative;
height: 120px;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.3) 0%, rgba(118, 75, 162, 0.3) 100%);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.crate-card-header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="vinyl" width="20" height="20" patternUnits="userSpaceOnUse"><circle cx="10" cy="10" r="8" fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="0.5"/></pattern></defs><rect width="100" height="100" fill="url(%23vinyl)"/></svg>');
opacity: 0.5;
}
.crate-icon {
font-size: 3.5rem;
color: white;
text-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
z-index: 1;
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-5px); }
}
.crate-card-body {
padding: 1.5rem;
}
.crate-card-title {
font-size: 1.4rem;
font-weight: 700;
color: white;
margin: 0 0 0.5rem 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.crate-card-title a {
color: inherit;
text-decoration: none;
transition: color 0.2s ease;
}
.crate-card-title a:hover {
color: #a5b4fc;
text-decoration: underline;
}
.crate-card-description {
font-size: 0.95rem;
color: #a0aec0;
margin: 0 0 1rem 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.crate-card-stats {
display: flex;
gap: 1.5rem;
margin-bottom: 1rem;
}
.crate-stat {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
color: #e0e0e0;
}
.crate-stat i {
color: #667eea;
font-size: 0.85rem;
}
/* Set Progress Bar */
.crate-set-progress {
margin-bottom: 1rem;
}
.crate-progress-bar {
height: 6px;
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
overflow: hidden;
margin-bottom: 0.5rem;
}
.crate-progress-fill {
height: 100%;
border-radius: 3px;
transition: width 0.3s ease;
}
.crate-progress-fill.complete {
background: linear-gradient(90deg, #10b981, #059669);
}
.crate-progress-fill.incomplete {
background: linear-gradient(90deg, #f59e0b, #d97706);
}
.crate-progress-label {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
color: #a0aec0;
}
.crate-progress-ready {
color: #10b981;
font-weight: 600;
}
/* Crate Actions */
.crate-card-actions {
display: flex;
gap: 0.75rem;
padding-top: 1rem;
border-top: 1px solid rgba(255, 255, 255, 0.08);
}
.crate-action-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.75rem;
border-radius: 10px;
border: none;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.crate-action-btn.play-btn {
background: linear-gradient(135deg, #10b981, #059669);
color: white;
}
.crate-action-btn.play-btn:hover {
transform: scale(1.02);
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
}
.crate-action-btn.view-btn {
background: rgba(102, 126, 234, 0.15);
color: #667eea;
border: 1px solid rgba(102, 126, 234, 0.3);
}
.crate-action-btn.view-btn:hover {
background: rgba(102, 126, 234, 0.25);
}
.crate-action-btn.share-btn {
background: rgba(255, 255, 255, 0.08);
color: #a0aec0;
flex: 0 0 auto;
width: 42px;
}
.crate-action-btn.share-btn:hover {
background: rgba(255, 255, 255, 0.15);
color: white;
}
/* Responsive */
@media (max-width: 768px) {
.cratelist-section {
padding: 3rem 0;
}
.cratelist-header {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.cratelist-title {
font-size: 2rem;
}
.cratelist-grid {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.crate-card-actions {
flex-wrap: wrap;
}
.crate-action-btn.share-btn {
flex: 1;
width: auto;
}
}
</style>
<script>
// Load public crates for this artist profile
document.addEventListener('DOMContentLoaded', function() {
loadArtistCrates(<?= $artist_id ?>);
});
function loadArtistCrates(artistId) {
const grid = document.getElementById('cratelistGrid');
const section = document.getElementById('cratelistSection');
fetch(`/api/get_artist_public_crates.php?artist_id=${artistId}`)
.then(response => response.json())
.then(data => {
if (data.success && data.crates && data.crates.length > 0) {
grid.innerHTML = data.crates.map(crate => renderCrateCard(crate)).join('');
} else {
// Hide entire section if no public crates
section.style.display = 'none';
}
})
.catch(error => {
console.error('Error loading crates:', error);
section.style.display = 'none';
});
}
function renderCrateCard(crate) {
return `
<div class="crate-card" onclick="openCrateModal(${crate.id}, '${escapeHtmlAttr(crate.name)}')">
<div class="crate-card-header">
<div class="crate-icon">đĻ</div>
</div>
<div class="crate-card-body">
<h3 class="crate-card-title" title="${escapeHtmlAttr(crate.name)}"><a href="/crate/${crate.id}/" onclick="event.stopPropagation();">${escapeHtml(crate.name)}</a></h3>
${crate.description ? `<p class="crate-card-description" title="${escapeHtmlAttr(crate.description)}">${escapeHtml(crate.description)}</p>` : ''}
<div class="crate-card-stats">
<div class="crate-stat">
<i class="fas fa-music"></i>
<span>${crate.track_count} <?= t('artist_profile.crate_tracks') ?></span>
</div>
<div class="crate-stat">
<i class="fas fa-clock"></i>
<span>${crate.duration_formatted}</span>
</div>
</div>
<div class="crate-card-actions">
<button class="crate-action-btn play-btn" onclick="event.stopPropagation(); playCrate(${crate.id})">
<i class="fas fa-play"></i>
<?= t('artist_profile.play_crate') ?>
</button>
<button class="crate-action-btn view-btn" onclick="event.stopPropagation(); openCrateModal(${crate.id}, '${escapeHtmlAttr(crate.name)}')">
<i class="fas fa-list"></i>
<?= t('artist_profile.view_crate') ?>
</button>
<button class="crate-action-btn share-btn" onclick="event.stopPropagation(); shareProfileCrate(${crate.id}, '${escapeHtmlAttr(crate.name)}')" title="<?= t('artist_profile.share_crate') ?>">
<i class="fas fa-share-alt"></i>
</button>
</div>
</div>
</div>
`;
}
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function escapeHtmlAttr(text) {
if (!text) return '';
return text.replace(/"/g, '"').replace(/'/g, ''');
}
function shareProfileCrate(crateId, crateName) {
const shareUrl = `${window.location.origin}/crate/${crateId}`;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(shareUrl)
.then(() => {
showNotification('<?= t('artist_profile.crate_link_copied') ?>', 'success');
})
.catch(() => {
fallbackCopyProfileCrate(shareUrl);
});
} else {
fallbackCopyProfileCrate(shareUrl);
}
}
function fallbackCopyProfileCrate(text) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
showNotification('<?= t('artist_profile.crate_link_copied') ?>', 'success');
} catch (err) {
console.error('Copy failed:', err);
}
document.body.removeChild(textarea);
}
function playCrate(crateId) {
showNotification('Loading crate...', 'info');
// Fetch crate tracks and play them all
fetch(`/api/get_crate_tracks.php?crate_id=${crateId}&public=1`)
.then(response => response.json())
.then(data => {
if (data.success && data.tracks && data.tracks.length > 0) {
// Store tracks for playlist
window.currentCrateTracksForModal = data.tracks;
// Set up playlist for Next/Previous
setupCratePlaylist(data.tracks, 0);
const firstTrack = data.tracks[0];
const title = firstTrack.title || 'Untitled';
const artist = firstTrack.artist_name || '';
const trackId = firstTrack.id;
const artistId = firstTrack.user_id || firstTrack.artist_id || null;
// Get signed URL
fetch(`/api/get_audio_token.php?track_id=${trackId}&duration=${firstTrack.duration || 300}`)
.then(response => response.json())
.then(tokenData => {
console.log('đĩ Audio token for crate:', tokenData);
if (!tokenData.success || !tokenData.url) {
throw new Error(tokenData.error || 'Failed to get audio token');
}
const signedUrl = tokenData.url;
// Correct param order: (audioUrl, title, artist, trackId, artistId)
if (window.enhancedGlobalPlayer && typeof window.enhancedGlobalPlayer.playTrack === 'function') {
window.enhancedGlobalPlayer.playTrack(signedUrl, title, artist, trackId, artistId);
showNotification(`Playing crate: ${data.tracks.length} tracks`, 'success');
} else if (typeof window.playTrack === 'function') {
window.playTrack(signedUrl, title, artist, trackId, artistId);
} else if (window.globalAudio) {
window.globalAudio.src = signedUrl;
window.globalAudio.play().catch(e => console.error('Playback error:', e));
} else {
showNotification('Player not available', 'error');
}
})
.catch(error => {
console.error('Error playing track:', error);
showNotification('Error loading track', 'error');
});
} else {
showNotification('No tracks in this crate', 'info');
}
})
.catch(error => {
console.error('Error playing crate:', error);
showNotification('Error loading crate', 'error');
});
}
function openCrateModal(crateId, crateName) {
// Use existing modal system or create a simple modal
if (typeof window.viewCrate === 'function') {
window.viewCrate(crateId);
return;
}
// Create simple modal for viewing crate tracks
const existingModal = document.getElementById('profileCrateModal');
if (existingModal) existingModal.remove();
const modal = document.createElement('div');
modal.id = 'profileCrateModal';
modal.className = 'profile-crate-modal';
modal.innerHTML = `
<div class="profile-crate-modal-content">
<div class="profile-crate-modal-header">
<h3><i class="fas fa-box-open"></i> ${escapeHtml(crateName)}</h3>
<button onclick="closeProfileCrateModal()" class="close-btn"><i class="fas fa-times"></i></button>
</div>
<div class="profile-crate-modal-body" id="profileCrateTracksList">
<div class="loading"><i class="fas fa-spinner fa-spin"></i> Loading tracks...</div>
</div>
</div>
`;
document.body.appendChild(modal);
document.body.style.overflow = 'hidden';
// Load tracks
fetch(`/api/get_crate_tracks.php?crate_id=${crateId}&public=1`)
.then(response => response.json())
.then(data => {
const tracksList = document.getElementById('profileCrateTracksList');
if (data.success && data.tracks && data.tracks.length > 0) {
// Store tracks for playback
window.currentCrateTracksForModal = data.tracks;
window.currentPlayingCrateIndex = -1;
tracksList.innerHTML = data.tracks.map((track, index) => {
const price = parseFloat(track.price) || 0;
// Track is public if it's in the public crate response (already filtered)
// Show cart if track has a price
const artistId = track.user_id || track.artist_id || '';
return `
<div class="profile-crate-track" data-track-id="${track.id}" data-index="${index}">
<div class="track-number">${index + 1}</div>
<button class="track-play-btn" data-index="${index}" onclick="togglePlayCrateTrack(${index}, this)">
<i class="fas fa-play"></i>
</button>
<div class="track-info">
<a href="/track/${track.id}" class="track-title-link" target="_blank" onclick="event.stopPropagation();">${escapeHtml(track.title || 'Untitled')}</a>
<a href="/artist/${artistId}" class="track-artist-link" target="_blank" onclick="event.stopPropagation();">${escapeHtml(track.artist_name || '')}</a>
</div>
<div class="track-duration">${formatDuration(track.duration || 0)}</div>
${price > 0 ? `<button class="track-cart-btn" onclick="addToCartFromCrateModal(${track.id}, '${escapeHtmlAttr(track.title || 'Track')}', ${price}, this)"><i class="fas fa-cart-plus"></i> $${price.toFixed(2)}</button>` : ''}
</div>
`;
}).join('');
} else {
tracksList.innerHTML = '<div class="empty-tracks">No tracks in this crate.</div>';
}
})
.catch(error => {
console.error('Error loading tracks:', error);
document.getElementById('profileCrateTracksList').innerHTML = '<div class="error">Failed to load tracks.</div>';
});
}
// Toggle play/pause for crate track
function togglePlayCrateTrack(index, button) {
const audioElement = document.getElementById('globalAudioElement');
const tracks = window.currentCrateTracksForModal;
if (!tracks || !tracks[index]) return;
const track = tracks[index];
const isCurrentlyPlaying = window.currentPlayingCrateIndex === index;
// If this track is currently playing, toggle pause/play
if (isCurrentlyPlaying && audioElement) {
if (audioElement.paused) {
audioElement.play();
updateCratePlayButtons(index, true);
} else {
audioElement.pause();
updateCratePlayButtons(index, false);
}
return;
}
// Play new track
window.currentPlayingCrateIndex = index;
playTrackFromCrateModal(index);
// Update button to show loading, will be updated to pause when actually playing
if (button) {
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
}
}
// Update all play buttons in crate modal
function updateCratePlayButtons(playingIndex, isPlaying) {
const allButtons = document.querySelectorAll('.profile-crate-track .track-play-btn');
allButtons.forEach((btn, idx) => {
const btnIndex = parseInt(btn.getAttribute('data-index'));
if (btnIndex === playingIndex && isPlaying) {
btn.innerHTML = '<i class="fas fa-pause"></i>';
btn.classList.add('playing');
} else {
btn.innerHTML = '<i class="fas fa-play"></i>';
btn.classList.remove('playing');
}
});
}
function closeProfileCrateModal() {
const modal = document.getElementById('profileCrateModal');
if (modal) {
modal.remove();
document.body.style.overflow = '';
}
}
function playTrackFromCrateModal(index) {
const tracks = window.currentCrateTracksForModal;
if (!tracks || !tracks[index]) {
console.warn('Track not found at index:', index);
return;
}
const track = tracks[index];
const title = track.title || 'Untitled';
const trackId = track.id;
const duration = track.duration || 300;
const artist = track.artist_name || '';
const artistId = track.user_id || track.artist_id || null;
console.log('đĩ Playing track from crate:', { trackId, title, artist, index });
// Show loading state on button
const playBtn = document.querySelector(`.profile-crate-track[data-track-id="${trackId}"] .track-play-btn`);
if (playBtn) {
playBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
playBtn.disabled = true;
}
// Get signed audio URL
fetch(`/api/get_audio_token.php?track_id=${trackId}&duration=${duration}`)
.then(response => response.json())
.then(data => {
console.log('đĩ Audio token response:', data);
if (!data.success || !data.url) {
throw new Error(data.error || 'Failed to get audio token');
}
const signedUrl = data.url;
// Update to pause button (track is playing)
window.currentPlayingCrateIndex = index;
updateCratePlayButtons(index, true);
if (playBtn) {
playBtn.disabled = false;
}
// Set up crate playlist for Next/Previous to work
setupCratePlaylist(tracks, index);
// Listen for pause/play events from global player to sync button state
const audioElement = document.getElementById('globalAudioElement');
if (audioElement) {
audioElement.onpause = function() {
if (window.currentPlayingCrateIndex === index) {
updateCratePlayButtons(index, false);
}
};
audioElement.onplay = function() {
if (window.currentPlayingCrateIndex === index) {
updateCratePlayButtons(index, true);
}
};
audioElement.onended = function() {
updateCratePlayButtons(-1, false);
window.currentPlayingCrateIndex = -1;
};
}
// Play using signed URL directly
// Correct param order: (audioUrl, title, artist, trackId, artistId)
if (window.enhancedGlobalPlayer && typeof window.enhancedGlobalPlayer.playTrack === 'function') {
console.log('đĩ Using enhancedGlobalPlayer');
window.enhancedGlobalPlayer.playTrack(signedUrl, title, artist, trackId, artistId);
} else if (typeof window.playTrack === 'function') {
console.log('đĩ Using window.playTrack');
window.playTrack(signedUrl, title, artist, trackId, artistId);
} else if (window.globalAudio) {
console.log('đĩ Using globalAudio directly');
window.globalAudio.src = signedUrl;
window.globalAudio.play().catch(e => console.error('Playback error:', e));
} else {
showNotification('Player not available. Please refresh the page.', 'error');
}
})
.catch(error => {
console.error('Error playing track:', error);
if (playBtn) {
playBtn.innerHTML = '<i class="fas fa-play"></i>';
playBtn.disabled = false;
}
window.currentPlayingCrateIndex = -1;
showNotification('Error loading track. Please try again.', 'error');
});
}
// Set up crate as playlist for Next/Previous functionality
function setupCratePlaylist(tracks, startIndex) {
if (!window.enhancedGlobalPlayer) {
console.warn('đĩ Global player not available for playlist');
return;
}
// Format tracks for the playlist
const formattedTracks = tracks.map(track => ({
id: track.id,
title: track.title || 'Untitled',
artist: track.artist_name || '',
audio_url: null, // Will be fetched with token when needed
duration: track.duration || 0,
user_id: track.user_id || track.artist_id || null
}));
// Set playlist on global player
window.enhancedGlobalPlayer.currentPlaylist = formattedTracks;
window.enhancedGlobalPlayer.currentPlaylistIndex = startIndex;
window.enhancedGlobalPlayer.currentPlaylistType = 'crate';
window.enhancedGlobalPlayer.autoPlayEnabled = true;
// Store crate tracks globally for the custom next/prev handler
window.cratePlaylistTracks = tracks;
window.cratePlaylistIndex = startIndex;
console.log('đĩ Crate playlist set up:', formattedTracks.length, 'tracks, starting at index', startIndex);
// Override the next/previous handlers for crate playback
window.cratePlayNext = function() {
const nextIndex = window.cratePlaylistIndex + 1;
if (nextIndex < window.cratePlaylistTracks.length) {
window.cratePlaylistIndex = nextIndex;
playTrackFromCrateModal(nextIndex);
} else {
console.log('đĩ End of crate playlist');
showNotification('End of crate', 'info');
}
};
window.cratePlayPrev = function() {
const prevIndex = window.cratePlaylistIndex - 1;
if (prevIndex >= 0) {
window.cratePlaylistIndex = prevIndex;
playTrackFromCrateModal(prevIndex);
} else {
console.log('đĩ Start of crate playlist');
}
};
// Listen for track end to auto-play next
const audioElement = document.getElementById('globalAudioElement');
if (audioElement) {
// Remove previous listener if any
audioElement.removeEventListener('ended', window.crateTrackEndHandler);
// Add new listener
window.crateTrackEndHandler = function() {
if (window.enhancedGlobalPlayer.currentPlaylistType === 'crate') {
window.cratePlayNext();
}
};
audioElement.addEventListener('ended', window.crateTrackEndHandler);
}
}
function addToCartFromCrateModal(trackId, title, price, button) {
console.log('đ Adding to cart from crate modal:', trackId, title, price);
// Add loading state
const originalHTML = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
button.disabled = true;
const formData = new FormData();
formData.append('action', 'add');
formData.append('track_id', trackId);
formData.append('artist_plan', 'free');
fetch('/cart.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
button.innerHTML = '<i class="fas fa-check"></i> Added';
button.classList.add('added');
button.style.background = 'rgba(16, 185, 129, 0.3)';
showNotification(`"${title}" added to cart!`, 'success');
// Update cart counter
const cartCounts = document.querySelectorAll('.cart-count');
if (data.cart_count !== undefined) {
cartCounts.forEach(count => {
count.textContent = data.cart_count;
count.style.display = data.cart_count > 0 ? 'flex' : 'none';
});
}
setTimeout(() => {
button.innerHTML = originalHTML;
button.classList.remove('added');
button.style.background = '';
button.disabled = false;
}, 3000);
} else {
if (data.already_in_cart) {
button.innerHTML = '<i class="fas fa-check"></i> In Cart';
showNotification('Track is already in your cart', 'info');
} else {
button.innerHTML = originalHTML;
showNotification(data.message || 'Failed to add to cart', 'error');
}
button.disabled = false;
}
})
.catch(error => {
console.error('Cart error:', error);
button.innerHTML = originalHTML;
button.disabled = false;
showNotification('Error adding to cart. Please try again.', 'error');
});
}
function formatDuration(seconds) {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
// Close modal on escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeProfileCrateModal();
}
});
// Hook into player Next/Previous buttons for crate playback
document.addEventListener('DOMContentLoaded', function() {
// Wait for player to be ready
setTimeout(function() {
const nextBtn = document.querySelector('.player-next-btn, .next-btn, [onclick*="playNext"], #playerNextBtn');
const prevBtn = document.querySelector('.player-prev-btn, .prev-btn, [onclick*="playPrev"], #playerPrevBtn');
// Also try the skip buttons in the global player
const skipNextBtn = document.getElementById('skipNext') || document.querySelector('[data-action="next"]');
const skipPrevBtn = document.getElementById('skipPrev') || document.querySelector('[data-action="prev"]');
function handleNextClick(e) {
if (window.enhancedGlobalPlayer && window.enhancedGlobalPlayer.currentPlaylistType === 'crate' && window.cratePlayNext) {
e.preventDefault();
e.stopPropagation();
window.cratePlayNext();
return false;
}
}
function handlePrevClick(e) {
if (window.enhancedGlobalPlayer && window.enhancedGlobalPlayer.currentPlaylistType === 'crate' && window.cratePlayPrev) {
e.preventDefault();
e.stopPropagation();
window.cratePlayPrev();
return false;
}
}
// Attach handlers
[nextBtn, skipNextBtn].forEach(btn => {
if (btn) {
btn.addEventListener('click', handleNextClick, true);
}
});
[prevBtn, skipPrevBtn].forEach(btn => {
if (btn) {
btn.addEventListener('click', handlePrevClick, true);
}
});
console.log('đĩ Crate Next/Prev handlers attached');
}, 2000);
});
</script>
<style>
/* Profile Crate Modal Styles */
.profile-crate-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(10px);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
animation: fadeIn 0.2s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.profile-crate-modal-content {
background: #1a1a1a;
border: 1px solid rgba(102, 126, 234, 0.3);
border-radius: 20px;
width: 90%;
max-width: 600px;
max-height: 80vh;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
}
.profile-crate-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 2rem;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.15) 0%, rgba(118, 75, 162, 0.15) 100%);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.profile-crate-modal-header h3 {
margin: 0;
color: white;
font-size: 1.4rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.75rem;
}
.profile-crate-modal-header h3 i {
color: #667eea;
}
.profile-crate-modal-header .close-btn {
background: rgba(255, 255, 255, 0.1);
border: none;
color: white;
width: 36px;
height: 36px;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.profile-crate-modal-header .close-btn:hover {
background: rgba(239, 68, 68, 0.3);
color: #ef4444;
}
.profile-crate-modal-body {
flex: 1;
overflow-y: auto;
padding: 1rem;
}
.profile-crate-modal-body .loading,
.profile-crate-modal-body .empty-tracks,
.profile-crate-modal-body .error {
text-align: center;
padding: 3rem;
color: #a0aec0;
font-size: 1.1rem;
}
.profile-crate-modal-body .loading i {
font-size: 2rem;
color: #667eea;
margin-right: 0.75rem;
}
.profile-crate-track {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
border-radius: 12px;
transition: background 0.2s ease;
}
.profile-crate-track:hover {
background: rgba(255, 255, 255, 0.05);
}
.profile-crate-track .track-number {
width: 24px;
text-align: center;
color: #666;
font-size: 0.9rem;
font-weight: 500;
}
.profile-crate-track .track-play-btn {
width: 36px;
height: 36px;
border-radius: 50%;
background: rgba(102, 126, 234, 0.2);
border: none;
color: #667eea;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.profile-crate-track .track-play-btn:hover {
background: #667eea;
color: white;
transform: scale(1.1);
}
.profile-crate-track .track-play-btn.playing {
background: #667eea;
color: white;
box-shadow: 0 0 12px rgba(102, 126, 234, 0.5);
}
.profile-crate-track .track-info {
flex: 1;
min-width: 0;
}
.profile-crate-track .track-title,
.profile-crate-track .track-title-link {
color: white;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-decoration: none;
display: block;
transition: all 0.2s ease;
cursor: pointer;
position: relative;
z-index: 10;
}
.profile-crate-track .track-title-link:hover {
color: #667eea;
text-decoration: underline;
}
.profile-crate-track .track-artist,
.profile-crate-track .track-artist-link {
font-size: 0.85rem;
color: #a0aec0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-decoration: none;
display: block;
transition: all 0.2s ease;
cursor: pointer;
position: relative;
z-index: 10;
}
.profile-crate-track .track-artist-link:hover {
color: #667eea;
text-decoration: underline;
}
.profile-crate-track .track-duration {
color: #666;
font-size: 0.9rem;
font-family: monospace;
}
.profile-crate-track .track-cart-btn {
background: linear-gradient(135deg, #10b981, #059669);
border: none;
color: white;
padding: 0.5rem 1rem;
border-radius: 8px;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.4rem;
transition: all 0.2s ease;
}
.profile-crate-track .track-cart-btn:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
}
</style>
<!-- Similar Artists and Statistics Section -->
<div class="artist-sections-container">
<!-- Similar Artists Section -->
<div class="enhanced-sidebar-section similar-artists-section">
<div class="section-header-modern">
<div class="header-content">
<div class="header-icon">đĩ</div>
<div class="header-text">
<h3 class="section-title-modern"><?= t('artist_profile.similar_artists') ?></h3>
<p class="section-subtitle"><?= t('artist_profile.similar_artists_desc') ?></p>
</div>
</div>
<div class="header-action">
<button class="refresh-btn" onclick="refreshSimilarArtists()">
<i class="fas fa-sync-alt"></i>
</button>
</div>
</div>
<div class="artists-grid">
<?php if (!empty($similar_artists)): ?>
<?php foreach ($similar_artists as $similar): ?>
<div class="artist-card-modern" onclick="loadArtistProfile(<?= $similar['id'] ?>)">
<div class="artist-avatar-modern">
<?php if (!empty($similar['profile_image'])): ?>
<img src="<?= htmlspecialchars($similar['profile_image']) ?>"
alt="<?= htmlspecialchars($similar['username']) ?>"
class="artist-avatar-img"
style="object-position: <?= htmlspecialchars($similar['profile_position'] ?? 'center center') ?>;">
<?php else: ?>
<div class="avatar-placeholder">
<span><?= strtoupper(substr($similar['username'], 0, 1)) ?></span>
</div>
<?php endif; ?>
</div>
<div class="artist-card-body">
<h4 class="artist-name-modern"><?= htmlspecialchars($similar['username']) ?></h4>
<div class="artist-meta">
<span class="meta-item">
<i class="fas fa-users"></i>
<?= number_format($similar['followers_count']) ?>
</span>
<span class="meta-item">
<i class="fas fa-music"></i>
<?= $similar['track_count'] ?>
</span>
<span class="meta-item">
<i class="fas fa-play"></i>
<?= number_format($similar['total_plays'] ?? 0) ?>
</span>
</div>
</div>
<div class="artist-card-footer">
<button class="follow-btn-mini <?= $similar['is_following'] ? 'following' : '' ?>" onclick="event.stopPropagation(); toggleFollow(<?= $similar['id'] ?>, this)">
<i class="fas fa-<?= $similar['is_following'] ? 'heart' : 'user-plus' ?>"></i>
<span><?= $similar['is_following'] ? t('artist_profile.following') : t('artist_profile.follow') ?></span>
</button>
</div>
</div>
<?php endforeach; ?>
<?php else: ?>
<div class="empty-state">
<div class="empty-icon">đ</div>
<h4><?= t('artist_profile.no_similar_artists') ?></h4>
<p><?= t('artist_profile.no_similar_artists_desc') ?></p>
</div>
<?php endif; ?>
</div>
</div>
<!-- Artist Statistics Section -->
<div class="enhanced-sidebar-section stats-section">
<div class="section-header-modern">
<div class="header-content">
<div class="header-icon">đ</div>
<div class="header-text">
<h3 class="section-title-modern"><?= t('artist_profile.artist_statistics') ?></h3>
<p class="section-subtitle"><?= t('artist_profile.performance_overview') ?></p>
</div>
</div>
<div class="header-action">
<button class="stats-toggle" onclick="toggleStatsView()">
<i class="fas fa-chart-line"></i>
</button>
</div>
</div>
<div class="stats-grid-modern">
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-music"></i>
</div>
<div class="stat-content">
<div class="stat-number"><?= number_format($artist['completed_tracks']) ?></div>
<div class="stat-label"><?= t('artist_profile.tracks_created') ?></div>
</div>
<div class="stat-trend positive">
<i class="fas fa-arrow-up"></i>
<span>12%</span>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-user-plus"></i>
</div>
<div class="stat-content">
<div class="stat-number"><?= number_format($artist['following_count']) ?></div>
<div class="stat-label"><?= t('artist_profile.following') ?></div>
</div>
<div class="stat-trend positive">
<i class="fas fa-arrow-up"></i>
<span>8%</span>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-headphones"></i>
</div>
<div class="stat-content">
<div class="stat-number"><?= number_format($artist['total_plays']) ?></div>
<div class="stat-label"><?= t('artist_profile.total_plays') ?></div>
</div>
<div class="stat-trend positive">
<i class="fas fa-arrow-up"></i>
<span>23%</span>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-heart"></i>
</div>
<div class="stat-content">
<div class="stat-number"><?= number_format($artist['total_likes']) ?></div>
<div class="stat-label"><?= t('artist_profile.total_likes') ?></div>
</div>
<div class="stat-trend positive">
<i class="fas fa-arrow-up"></i>
<span>15%</span>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-clock"></i>
</div>
<div class="stat-content">
<div class="stat-number"><?= number_format(floor($artist['total_duration'] / 3600)) ?></div>
<div class="stat-label"><?= t('artist_profile.hours_of_music') ?></div>
</div>
<div class="stat-trend neutral">
<i class="fas fa-minus"></i>
<span>0%</span>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-ticket-alt"></i>
</div>
<div class="stat-content">
<div class="stat-number"><?= number_format($artist['events_count']) ?></div>
<div class="stat-label"><?= t('artist_profile.events_hosted') ?></div>
</div>
<div class="stat-trend neutral">
<i class="fas fa-minus"></i>
<span>0%</span>
</div>
</div>
</div>
</div>
</div>
<!-- Essential JavaScript Functions -->
<script>
const ratingLabelMap = <?= json_encode([
'1' => t('artist_profile.rating_label_1'),
'2' => t('artist_profile.rating_label_2'),
'3' => t('artist_profile.rating_label_3'),
'4' => t('artist_profile.rating_label_4'),
'5' => t('artist_profile.rating_label_5'),
'6' => t('artist_profile.rating_label_6'),
'7' => t('artist_profile.rating_label_7'),
'8' => t('artist_profile.rating_label_8'),
'9' => t('artist_profile.rating_label_9'),
'10' => t('artist_profile.rating_label_10'),
'default' => t('artist_profile.rating_label_default'),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
// Follow/Unfollow functionality
function toggleFollow(artistId, button) {
if (!<?= isset($_SESSION['user_id']) ? 'true' : 'false' ?>) {
if (typeof showNotification === 'function') {
showNotification(notificationTranslations.please_log_in_follow, 'error');
} else {
alert(notificationTranslations.please_log_in_follow);
}
return;
}
const isFollowing = button.classList.contains('following');
// Disable button during request
button.disabled = true;
const originalHTML = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i><span>' + notificationTranslations.loading + '</span>';
fetch('api_toggle_follow.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_id: artistId })
})
.then(response => response.json())
.then(data => {
button.disabled = false;
if (data.success) {
if (data.action === 'unfollowed') {
button.classList.remove('following');
button.innerHTML = '<i class="fas fa-user-plus"></i><span><?= t('artist_profile.follow') ?></span>';
if (typeof showNotification === 'function') {
showNotification(notificationTranslations.unfollowed_successfully, 'success');
}
} else if (data.action === 'followed') {
button.classList.add('following');
button.innerHTML = '<i class="fas fa-heart"></i><span><?= t('artist_profile.following') ?></span>';
if (typeof showNotification === 'function') {
showNotification(notificationTranslations.following_artist, 'success');
}
}
} else {
button.innerHTML = originalHTML;
if (typeof showNotification === 'function') {
showNotification(data.error || data.message || notificationTranslations.failed_update_follow, 'error');
}
}
})
.catch(error => {
button.disabled = false;
button.innerHTML = originalHTML;
console.error('Follow toggle error:', error);
if (typeof showNotification === 'function') {
showNotification(notificationTranslations.failed_update_follow + '. ' + notificationTranslations.please_try_again, 'error');
}
});
}
function openArtistEventModal(eventId) {
if (!eventId) return;
if (typeof window.showEventDetails === 'function') {
window.showEventDetails(eventId);
return;
}
const existingOverlay = document.getElementById('eventModalOverlay');
if (existingOverlay) {
existingOverlay.remove();
document.body.style.overflow = '';
}
const loader = document.createElement('div');
loader.className = 'event-modal-loader';
loader.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.85);display:flex;align-items:center;justify-content:center;color:#fff;font-size:1.4rem;z-index:99999;';
loader.innerHTML = '<div><i class="fas fa-spinner fa-spin"></i> Loading event...</div>';
document.body.appendChild(loader);
fetch(`/event_modal.php?id=${eventId}&ajax=1&from=profile&t=${Date.now()}`)
.then(resp => {
if (!resp.ok) throw new Error('Failed to load event');
return resp.json().catch(() => ({}));
})
.then(payload => {
loader.remove();
const markup = typeof payload.html === 'string' ? payload.html : '';
if (!markup || markup.trim().length === 0) {
throw new Error('Modal content missing');
}
document.body.insertAdjacentHTML('beforeend', markup);
const overlay = document.getElementById('eventModalOverlay');
if (!overlay) {
throw new Error('Modal overlay not found in response');
}
const scripts = document.querySelectorAll('script[data-event-modal-script=\"1\"]');
scripts.forEach(script => {
const newScript = document.createElement('script');
if (script.src) {
newScript.src = script.src;
} else {
newScript.textContent = script.textContent;
}
document.body.appendChild(newScript);
script.remove();
});
overlay.style.display = 'flex';
overlay.style.visibility = 'visible';
overlay.style.opacity = '1';
overlay.style.zIndex = '99999';
const initHandlers = (attempt = 0) => {
if (typeof window.setupModalHandlers === 'function') {
window.setupModalHandlers(overlay, eventId);
} else if (attempt < 20) {
setTimeout(() => initHandlers(attempt + 1), 50);
} else {
console.warn('setupModalHandlers not available after waiting');
}
};
initHandlers();
setTimeout(() => {
try {
if (typeof window.initEventModalCountdown === 'function') {
window.initEventModalCountdown();
}
} catch (e) {
console.error('Countdown init error:', e);
}
try {
if (typeof window.initFloatingAttendees === 'function') {
window.initFloatingAttendees();
}
} catch (e) {
console.error('Floating attendees init error:', e);
}
}, 200);
document.body.style.overflow = 'hidden';
})
.catch(err => {
console.error('Event modal error:', err);
loader.remove();
alert('Unable to load event details. Please try again.');
});
}
// Load artist profile
function loadArtistProfile(artistId) {
window.location.href = `artist_profile_clean.php?id=${artistId}`;
}
// Toggle friend/unfriend functionality
function toggleFriend(artistId, button) {
console.log('toggleFriend called:', { artistId, button });
if (!<?= isset($_SESSION['user_id']) ? 'true' : 'false' ?>) {
if (typeof showNotification === 'function') {
showNotification(notificationTranslations.please_log_in_friends, 'error');
} else {
alert(notificationTranslations.please_log_in_friends);
}
return;
}
const isFriend = button.classList.contains('friend');
const currentStatus = button.getAttribute('data-current-status');
console.log('Current friend status:', { isFriend, currentStatus });
// Disable button during request
button.disabled = true;
const originalHTML = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i><span>' + notificationTranslations.loading + '</span>';
fetch('api_social.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'friend_request',
friend_id: artistId
})
})
.then(response => {
console.log('Friend API response status:', response.status);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
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('Friend API response data:', data);
button.disabled = false;
if (data.success) {
if (data.action === 'unfriended') {
// Update to "Add Friend"
button.classList.remove('friend');
button.setAttribute('data-current-status', 'not_friends');
button.innerHTML = '<i class="fas fa-user-plus"></i><span><?= t('artist_profile.add_friend') ?></span>';
if (typeof showNotification === 'function') {
showNotification('Unfriended successfully', 'success');
}
} else if (data.action === 'friend_request_sent') {
// Friend request sent (pending)
button.classList.remove('friend');
button.classList.add('pending');
button.setAttribute('data-current-status', 'pending');
button.innerHTML = '<i class="fas fa-clock"></i><span>Pending</span>';
button.disabled = true; // Disable button while pending
if (typeof showNotification === 'function') {
showNotification('Friend request sent', 'success');
}
} else if (data.action === 'friend_accepted') {
// Friend request accepted
button.classList.add('friend');
button.setAttribute('data-current-status', 'friends');
button.innerHTML = '<i class="fas fa-user-times"></i><span>Unfriend</span>';
if (typeof showNotification === 'function') {
showNotification('Friend request accepted', 'success');
}
} else {
console.warn('Unknown action:', data.action);
}
} else {
button.innerHTML = originalHTML;
const errorMsg = data.message || 'Failed to update friend status';
console.error('Friend toggle failed:', errorMsg);
if (typeof showNotification === 'function') {
showNotification(errorMsg, 'error');
} else {
alert(errorMsg);
}
}
})
.catch(error => {
button.disabled = false;
button.innerHTML = originalHTML;
console.error('Friend toggle error:', error);
const errorMsg = 'Failed to update friend status. Please try again.';
if (typeof showNotification === 'function') {
showNotification(errorMsg, 'error');
} else {
alert(errorMsg);
}
});
}
// Open message modal
function openMessageModal(artistId, artistName) {
if (!<?= isset($_SESSION['user_id']) ? 'true' : 'false' ?>) {
if (typeof showNotification === 'function') {
showNotification(notificationTranslations.please_log_in_messages, 'error');
} else {
alert(notificationTranslations.please_log_in_messages);
}
return;
}
// Remove existing modal if any
const existingModal = document.getElementById('messageModal');
if (existingModal) {
existingModal.remove();
}
// Create modal HTML
const modalHTML = `
<div id="messageModal" class="message-modal">
<div class="message-modal-content">
<div class="message-modal-header">
<h3>
<a href="/messages.php?user_id=${artistId}" class="message-modal-title-link" onclick="closeMessageModal()">
<i class="fas fa-envelope"></i> Message ${artistName}
</a>
</h3>
<button class="close-modal" onclick="closeMessageModal()">×</button>
</div>
<div class="message-modal-body">
<div id="messageHistory" class="message-history">
<div class="loading-messages">Loading messages...</div>
</div>
<div class="message-input-container">
<textarea id="messageInput" placeholder="Type your message..." maxlength="5000" rows="3"></textarea>
<button onclick="sendMessageToArtist(${artistId})" class="send-message-btn" id="sendMessageBtn">
<i class="fas fa-paper-plane"></i> Send
</button>
</div>
</div>
</div>
</div>
`;
// Add modal to page
document.body.insertAdjacentHTML('beforeend', modalHTML);
// Load existing messages
loadMessagesForArtist(artistId);
// Focus on input
setTimeout(() => {
const messageInput = document.getElementById('messageInput');
if (messageInput) {
messageInput.focus();
// Enter key to send (Shift+Enter for new line)
messageInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessageToArtist(artistId);
}
});
}
}, 100);
// Close on outside click
document.getElementById('messageModal').addEventListener('click', function(e) {
if (e.target === this) {
closeMessageModal();
}
});
}
// Close message modal
function closeMessageModal() {
const modal = document.getElementById('messageModal');
if (modal) {
modal.remove();
}
}
// Load messages for artist
function loadMessagesForArtist(artistId) {
const messageHistory = document.getElementById('messageHistory');
if (!messageHistory) return;
fetch(`api/messages.php?action=get_messages&user_id=${artistId}`)
.then(response => response.json())
.then(data => {
if (data.success && data.messages) {
messageHistory.innerHTML = '';
if (data.messages.length === 0) {
messageHistory.innerHTML = '<div class="no-messages">No messages yet. Start the conversation!</div>';
} else {
data.messages.forEach(message => {
const messageDiv = document.createElement('div');
messageDiv.className = `message-item ${message.sender_id == <?= $_SESSION['user_id'] ?? 0 ?> ? 'sent' : 'received'}`;
messageDiv.innerHTML = `
<div class="message-content">${message.message}</div>
<div class="message-time">${formatMessageTime(message.created_at)}</div>
`;
messageHistory.appendChild(messageDiv);
});
// Scroll to bottom
messageHistory.scrollTop = messageHistory.scrollHeight;
}
} else {
messageHistory.innerHTML = '<div class="error-message">Failed to load messages</div>';
}
})
.catch(error => {
console.error('Error loading messages:', error);
messageHistory.innerHTML = '<div class="error-message">Error loading messages</div>';
});
}
// Send message to artist
function sendMessageToArtist(artistId) {
const messageInput = document.getElementById('messageInput');
const sendBtn = document.getElementById('sendMessageBtn');
if (!messageInput || !sendBtn) return;
const message = messageInput.value.trim();
if (!message) return;
// Disable button
sendBtn.disabled = true;
sendBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Sending...';
const formData = new FormData();
formData.append('action', 'send_message');
formData.append('receiver_id', artistId);
formData.append('message', message);
fetch('api/messages.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
messageInput.value = '';
// Reload messages
loadMessagesForArtist(artistId);
// Update header badge if function exists
if (window.updateMessagesBadge) {
window.updateMessagesBadge();
}
} else {
if (typeof showNotification === 'function') {
showNotification(data.message || 'Error sending message', 'error');
} else {
alert(data.message || 'Error sending message');
}
}
})
.catch(error => {
console.error('Error sending message:', error);
if (typeof showNotification === 'function') {
showNotification('Error sending message', 'error');
} else {
alert('Error sending message');
}
})
.finally(() => {
sendBtn.disabled = false;
sendBtn.innerHTML = '<i class="fas fa-paper-plane"></i> Send';
});
}
// Format message time
function formatMessageTime(timestamp) {
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
const minutes = Math.floor(diff / 60000);
const hours = Math.floor(diff / 3600000);
const days = Math.floor(diff / 86400000);
if (minutes < 1) return 'Just now';
if (minutes < 60) return `${minutes}m ago`;
if (hours < 24) return `${hours}h ago`;
if (days < 7) return `${days}d ago`;
return date.toLocaleDateString();
}
// Share artist profile (using community_fixed.php pattern)
function shareArtist() {
const shareButton = document.querySelector('.share-btn');
const artistName = document.querySelector('.artist-name')?.textContent?.trim() || 'Artist';
let url = window.location.href;
// Prefer custom URL if available
if (shareButton) {
const customUrl = shareButton.getAttribute('data-custom-url');
const artistId = shareButton.getAttribute('data-artist-id');
if (customUrl && customUrl.trim() !== '') {
url = window.location.origin + '/' + customUrl;
} else if (artistId) {
url = window.location.origin + '/artist_profile.php?id=' + artistId;
}
}
const shareText = `Check out ${artistName} on SoundStudioPro! đĩ`;
const shareData = {
title: `${artistName} on SoundStudioPro`,
text: shareText,
url: url
};
// Try native share API first (mobile devices)
if (navigator.share) {
if (navigator.canShare && navigator.canShare(shareData)) {
navigator.share(shareData)
.then(() => {
if (typeof showNotification === 'function') {
showNotification('â
Profile shared successfully!', 'success');
}
})
.catch((error) => {
if (error.name !== 'AbortError') {
copyToClipboardFallback(url, shareText);
}
});
} else if (!navigator.canShare) {
navigator.share(shareData)
.then(() => {
if (typeof showNotification === 'function') {
showNotification('â
Profile shared successfully!', 'success');
}
})
.catch((error) => {
if (error.name !== 'AbortError') {
copyToClipboardFallback(url, shareText);
}
});
} else {
copyToClipboardFallback(url, shareText);
}
} else {
copyToClipboardFallback(url, shareText);
}
}
// Copy to clipboard fallback (matching community_fixed.php pattern)
function copyToClipboardFallback(url, shareText) {
const shareData = `${shareText}\n${url}`;
const isSecureContext = window.isSecureContext || location.protocol === 'https:' || location.hostname === 'localhost';
if (isSecureContext && navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(shareData)
.then(() => {
if (typeof showNotification === 'function') {
showNotification('đ Link copied to clipboard!', 'success');
} else {
alert('Link copied to clipboard!');
}
})
.catch(() => {
fallbackCopyMethod(shareData);
});
} else {
fallbackCopyMethod(shareData);
}
}
// Fallback copy using textarea (better mobile support)
function fallbackCopyMethod(shareData) {
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', '');
document.body.appendChild(textarea);
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';
}
textarea.focus();
textarea.select();
textarea.setSelectionRange(0, shareData.length);
try {
const successful = document.execCommand('copy');
document.body.removeChild(textarea);
if (successful) {
if (typeof showNotification === 'function') {
showNotification('đ Link copied to clipboard!', 'success');
} else {
alert('Link copied to clipboard!');
}
} else {
prompt('Copy this link:', shareData);
}
} catch (error) {
document.body.removeChild(textarea);
prompt('Copy this link:', shareData);
}
}
// Show comments - redirects to track page with comments section
function showComments(trackId) {
window.location.href = `/track.php?id=${trackId}#comments`;
}
// Show/hide variations for a track
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');
}
}
// Share track from card (matching community_fixed.php pattern)
function shareTrackFromCard(trackId, trackTitle, artistName) {
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
fetch('/api_social.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'share',
track_id: trackId,
platform: 'web'
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
console.log('đ Share recorded for track:', trackId);
// Update share count in UI
const shareBtn = document.querySelector(`.music-card[data-track-id="${trackId}"] .share-track-btn span`);
if (shareBtn && data.share_count !== undefined) {
shareBtn.textContent = parseInt(data.share_count).toLocaleString();
}
}
})
.catch(error => {
console.error('đ Share recording error:', error);
});
// Try native share API first
if (navigator.share) {
if (navigator.canShare && navigator.canShare(shareData)) {
navigator.share(shareData)
.then(() => {
if (typeof showNotification === 'function') {
showNotification('â
Track shared successfully!', 'success');
}
})
.catch((error) => {
if (error.name !== 'AbortError') {
copyToClipboardFallback(url, shareText);
}
});
} else if (!navigator.canShare) {
navigator.share(shareData)
.then(() => {
if (typeof showNotification === 'function') {
showNotification('â
Track shared successfully!', 'success');
}
})
.catch((error) => {
if (error.name !== 'AbortError') {
copyToClipboardFallback(url, shareText);
}
});
} else {
copyToClipboardFallback(url, shareText);
}
} else {
copyToClipboardFallback(url, shareText);
}
}
// Purchase track
function purchaseTrack(trackId, title, price) {
// Redirect to purchase system
window.location.href = `purchase.php?track_id=${trackId}`;
}
// Toggle track like
function toggleTrackLike(trackId, button) {
if (!trackId || !button) return;
button.classList.add('loading');
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 update like.', 'error');
} else {
alert(data.error || 'Unable to update like.');
}
return;
}
button.classList.toggle('liked', !!data.liked);
button.setAttribute('aria-pressed', data.liked ? 'true' : 'false');
const countSpan = button.querySelector('.like-count');
if (countSpan) {
countSpan.textContent = (data.like_count || 0).toLocaleString();
}
})
.catch(error => {
console.error('toggleTrackLike error', error);
if (typeof showNotification === 'function') {
showNotification(notificationTranslations.network_error_like, 'error');
} else {
alert('Network error while updating like. Please try again.');
}
})
.finally(() => {
button.classList.remove('loading');
});
}
// Voting functionality for track cards
function voteTrackFromCard(trackId, voteType, button) {
const isLoggedIn = <?= isset($_SESSION['user_id']) && $_SESSION['user_id'] ? 'true' : 'false' ?>;
if (!isLoggedIn) {
const loginToVote = '<?= addslashes(t('artist_profile.login_to_vote')) ?>';
if (typeof showNotification === 'function') {
showNotification(loginToVote, 'error');
} else {
alert(loginToVote);
}
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 errorVoting = '<?= addslashes(t('artist_profile.error_voting')) ?>';
const errorMsg = data.message || errorVoting;
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 errorVotingRetry = '<?= addslashes(t('artist_profile.error_voting_retry')) ?>';
const errorMsg = error.message || errorVotingRetry;
if (typeof showNotification === 'function') {
showNotification(errorMsg, 'error');
} else {
alert(errorMsg);
}
})
.finally(() => {
// Re-enable buttons
if (upBtn) upBtn.style.pointerEvents = 'auto';
if (downBtn) downBtn.style.pointerEvents = 'auto';
});
}
// Add to cart functionality
function addToCart(trackId, title, price, button) {
console.log('đ Adding to cart:', trackId, title, price);
// Add loading state
const originalHTML = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
button.style.pointerEvents = 'none';
// Use the proper cart API endpoint
const formData = new FormData();
formData.append('action', 'add');
formData.append('track_id', trackId);
formData.append('artist_plan', 'free'); // Default, can be updated based on actual artist plan
console.log('đ Sending cart request:', {
action: 'add',
track_id: trackId,
artist_plan: 'free'
});
fetch('/cart.php', {
method: 'POST',
body: formData
})
.then(response => {
console.log('đ Cart API response status:', response.status);
return response.json();
})
.then(data => {
console.log('đ Cart API response data:', data);
if (data.success) {
// Show success state
button.innerHTML = '<i class="fas fa-check"></i> ' + notificationTranslations.added;
button.classList.add('added');
showNotification(notificationTranslations.added_to_cart.replace(':title', title).replace(':price', price), 'success');
// Update cart counter if it exists - use the actual count from the API response
const cartCounts = document.querySelectorAll('.cart-count');
if (cartCounts.length > 0 && data.cart_count !== undefined) {
cartCounts.forEach(count => {
count.textContent = data.cart_count;
// Show the badge if count > 0
if (data.cart_count > 0) {
count.style.display = 'flex';
} 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';
});
}
// Refresh cart modal if it's currently open
const cartModal = document.getElementById('cartModal');
if (cartModal && cartModal.style.display === 'flex') {
if (typeof refreshCartModal === 'function') {
refreshCartModal();
}
}
// Reset button after delay
setTimeout(() => {
button.innerHTML = originalHTML;
button.classList.remove('added');
button.style.pointerEvents = 'auto';
}, 3000);
} else {
// Handle already in cart case - show warning with translated message
if (data.already_in_cart) {
const message = data.message || '<?= t('cart.error.already_in_cart') ?>';
showNotification(message, 'warning');
} else {
// Other errors
const errorMsg = data.message || 'Failed to add to cart';
showNotification('Error: ' + errorMsg, 'error');
}
// Restore button
button.innerHTML = originalHTML;
button.style.pointerEvents = 'auto';
}
})
.catch(error => {
console.error('â Cart error:', error);
showNotification(notificationTranslations.failed_add_cart + ': ' + error.message, 'error');
// Restore button
button.innerHTML = originalHTML;
button.style.pointerEvents = 'auto';
});
}
// Attach play button event listeners to dynamically loaded buttons
function attachPlayButtonListeners() {
const newButtons = document.querySelectorAll('.play-track-btn:not([data-listener-attached]), .variation-play-btn:not([data-listener-attached])');
const artistIdForPlay = <?= $artist_id ?>;
const artistNameForPlay = '<?= htmlspecialchars($artist['username'] ?? 'Artist', ENT_QUOTES, 'UTF-8') ?>';
newButtons.forEach(button => {
button.setAttribute('data-listener-attached', 'true');
button.addEventListener('click', async function(e) {
e.preventDefault();
e.stopPropagation();
const audioUrl = this.getAttribute('data-audio-url');
const title = this.getAttribute('data-title');
const artist = this.getAttribute('data-artist') || artistNameForPlay;
const trackId = this.getAttribute('data-track-id');
console.log('đĩ Dynamic play button clicked:', { trackId, audioUrl, title });
if (!audioUrl || audioUrl === 'null' || audioUrl === '') {
if (typeof showNotification === 'function') {
showNotification('Audio not available for playback', 'error');
}
return;
}
// Clear other playing states
document.querySelectorAll('.play-track-btn, .variation-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';
// Use global player
if (window.enhancedGlobalPlayer && typeof window.enhancedGlobalPlayer.playTrack === 'function') {
window.enhancedGlobalPlayer.playTrack(audioUrl, title, artist, trackId, artistIdForPlay);
} else {
console.error('Global player not available');
if (typeof showNotification === 'function') {
showNotification('Audio player not ready', 'error');
}
}
});
});
}
// Load more tracks
let currentTrackOffset = <?= count($tracks) ?>;
const tracksPerPage = 20;
const artistId = <?= $artist_id ?>;
function loadMoreTracks() {
const loadMoreBtn = document.querySelector('.load-more-btn');
if (!loadMoreBtn) return;
const originalText = loadMoreBtn.innerHTML;
loadMoreBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> <?= t('common.loading') ?? 'Loading...' ?>';
loadMoreBtn.disabled = true;
fetch(`/api/get_artist_tracks.php?artist_id=${artistId}&type=completed&offset=${currentTrackOffset}&limit=${tracksPerPage}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log('Load more tracks response:', data);
if (data.success && data.tracks && data.tracks.length > 0) {
// Try multiple selectors to find the tracks container
const tracksContainer = document.querySelector('.music-grid') ||
document.querySelector('.tracks-grid') ||
document.querySelector('.tracks-container');
if (!tracksContainer) {
console.error('Tracks container not found. Available containers:', {
musicGrid: document.querySelector('.music-grid'),
tracksGrid: document.querySelector('.tracks-grid'),
tracksContainer: document.querySelector('.tracks-container')
});
loadMoreBtn.innerHTML = originalText;
loadMoreBtn.disabled = false;
return;
}
// Render new tracks
let tracksAdded = 0;
data.tracks.forEach(track => {
try {
const trackCard = createTrackCard(track);
if (trackCard) {
tracksContainer.insertAdjacentHTML('beforeend', trackCard);
tracksAdded++;
} else {
console.error('createTrackCard returned empty for track:', track.id);
}
} catch (error) {
console.error('Error creating track card for track:', track.id, error);
}
});
console.log(`Added ${tracksAdded} tracks to container`);
// Attach event listeners to newly added play buttons
if (typeof attachPlayButtonListeners === 'function') {
attachPlayButtonListeners();
} else {
console.warn('attachPlayButtonListeners function not found');
}
currentTrackOffset += data.tracks.length;
// Hide button if no more tracks
if (data.tracks.length < tracksPerPage) {
loadMoreBtn.style.display = 'none';
} else {
loadMoreBtn.innerHTML = originalText;
loadMoreBtn.disabled = false;
}
} else {
// No more tracks
loadMoreBtn.style.display = 'none';
if (typeof showNotification === 'function') {
showNotification('<?= t('artist_profile.no_more_tracks') ?? 'No more tracks available' ?>', 'info');
}
}
})
.catch(error => {
console.error('Error loading more tracks:', error);
loadMoreBtn.innerHTML = originalText;
loadMoreBtn.disabled = false;
if (typeof showNotification === 'function') {
showNotification('<?= t('common.error_loading') ?? 'Error loading tracks. Please try again.' ?>', 'error');
}
});
}
// Helper function to create track card HTML (matches the PHP structure)
function createTrackCard(track) {
// Extract metadata
const metadata = track.metadata ? (typeof track.metadata === 'string' ? JSON.parse(track.metadata) : track.metadata) : {};
const genre = metadata.genre || track.genre || 'Electronic';
const mood = metadata.mood || null;
// Handle image URL
let imageUrl = track.image_url || null;
if (imageUrl && !imageUrl.startsWith('/') && !imageUrl.startsWith('http')) {
imageUrl = '/' + imageUrl;
}
if (!imageUrl || imageUrl === 'null') {
imageUrl = '/assets/images/default-track.jpg';
}
const price = track.price && parseFloat(track.price) > 0 ? parseFloat(track.price) : 1.99;
const displayTitle = track.title || 'Untitled Track';
const duration = track.duration ? Math.floor(track.duration / 60) + ':' + String(Math.floor(track.duration % 60)).padStart(2, '0') : '0:00';
const userLiked = track.user_liked ? 'liked' : '';
const isWishlisted = track.is_in_wishlist ? 'active' : '';
const userPurchased = track.user_purchased;
// Escape HTML
const escapeHtml = (text) => {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
};
const escapedTitle = escapeHtml(displayTitle);
const escapedGenre = escapeHtml(genre);
const escapedMood = mood ? escapeHtml(mood) : '';
return `
<div class="music-card" data-track-id="${track.id}">
<div class="music-artwork">
${imageUrl && imageUrl !== '/assets/images/default-track.jpg' ?
`<img src="${imageUrl}" alt="${escapedTitle}" class="track-cover-image" loading="lazy">` :
`<div class="artwork-placeholder"><span>${displayTitle.charAt(0).toUpperCase()}</span></div>`
}
<div class="track-duration-badge">${duration}</div>
<div class="artwork-overlay">
<button class="overlay-play-btn play-track-btn"
data-audio-url="${track.signed_audio_url || ''}"
data-title="${escapedTitle}"
data-artist="${escapeHtml(track.artist_name || '')}"
data-track-id="${track.id}">
<i class="fas fa-play"></i>
</button>
</div>
</div>
<div class="music-info">
<h3 class="music-title">
<a href="/track.php?id=${track.id}" class="track-title-link" title="${escapedTitle}">
${escapedTitle}
</a>
${price > 0 ? `<span class="for-sale-chip"><i class="fas fa-tag"></i> $${price}</span>` : ''}
</h3>
<div class="music-meta">
<a href="/community_fixed.php?genre=${encodeURIComponent(genre)}" class="genre-tag">${escapedGenre}</a>
${mood && mood !== genre && mood.toLowerCase() !== 'neutral' ?
`<a href="/community_fixed.php?genre=${encodeURIComponent(mood)}" class="mood-tag">${escapedMood}</a>` : ''
}
</div>
</div>
<div class="card-footer">
<div class="track-stats">
<span class="stat-item" title="<?= addslashes(t('artist_profile.plays')) ?>">
<i class="fas fa-headphones-alt"></i>
${(track.play_count || 0).toLocaleString()}
</span>
<button class="stat-item like-toggle ${userLiked}"
onclick="toggleTrackLike(${track.id}, this)"
title="<?= addslashes(t('artist_profile.like')) ?>">
<i class="fas fa-heart"></i>
<span class="like-count">${(track.like_count || 0).toLocaleString()}</span>
</button>
<button class="stat-item comment-btn"
onclick="showComments(${track.id})"
title="<?= addslashes(t('artist_profile.comments')) ?>">
<i class="fas fa-comment"></i>
<span>${(track.comment_count || 0).toLocaleString()}</span>
</button>
<button class="stat-item share-track-btn"
onclick="shareTrackFromCard(${track.id}, '${displayTitle.replace(/'/g, "\\'")}', '${(track.artist_name || '').replace(/'/g, "\\'")}')"
title="<?= addslashes(t('artist_profile.share_track')) ?>">
<i class="fas fa-share"></i>
<span>${(track.share_count || 0).toLocaleString()}</span>
</button>
${window.userIsLoggedIn ? `<button class="stat-item add-to-crate-btn"
onclick="openAddToCrateModal(${track.id}, '${displayTitle.replace(/'/g, "\\'")}')"
title="<?= t('library.crates.add_to_crate') ?>">
<i class="fas fa-box"></i>
</button>` : ''}
${(track.rating_count || 0) > 0 ?
`<span class="stat-item" onclick="showTrackRatingModal(${track.id}, '${displayTitle.replace(/'/g, "\\'")}')" title="Rating" style="cursor: pointer;">
<i class="fas fa-star" style="color: #fbbf24;"></i>
${(track.average_rating || 0).toFixed(1)}
</span>` : ''
}
${track.variations && track.variations.length > 0 ?
`<button class="stat-item variations-btn"
onclick="showVariations(${track.id}, this)"
title="${track.variations.length} <?= t('community.variations') ?? 'Variations' ?>">
<i class="fas fa-layer-group"></i>
<span>${track.variations.length}</span>
</button>` : ''
}
</div>
</div>
<div class="card-purchase">
<div class="price-display">
<div class="current-price">$${price}</div>
</div>
${userPurchased ?
`<button class="add-to-cart-btn" disabled>
<i class="fas fa-check"></i>
<?= t('artist_profile.purchased') ?? 'Purchased' ?>
</button>` :
`<button class="add-to-cart-btn" onclick="addToCart(${track.id}, '${displayTitle.replace(/'/g, "\\'")}', ${price}, this)">
<i class="fas fa-cart-plus"></i>
<?= t('artist_profile.add_to_cart') ?>
</button>`
}
<button class="wishlist-btn ${isWishlisted}"
onclick="toggleWishlist(${track.id}, this)"
title="${isWishlisted ? '<?= addslashes(t('artist_profile.remove_from_wishlist')) ?>' : '<?= addslashes(t('artist_profile.add_to_wishlist')) ?>'}">
<i class="${isWishlisted ? 'fas' : 'far'} fa-heart"></i>
</button>
</div>
${track.variations && track.variations.length > 0 ? `
<div class="variations-container" id="variations-${track.id}" style="display: none;">
<div class="variations-header">
<span><?= t('community.audio_variations') ?? 'Audio Variations' ?> (${track.variations.length})</span>
</div>
<div class="variations-grid">
${track.variations.map((v, idx) => {
const varTitle = displayTitle + ' - <?= t('community.variation') ?? 'Variation' ?> ' + ((v.variation_index !== null && v.variation_index !== undefined) ? (parseInt(v.variation_index) + 1) : (idx + 1));
const varDuration = v.duration ? Math.floor(v.duration / 60) + ':' + String(Math.floor(v.duration % 60)).padStart(2, '0') : '0:00';
return `
<div class="variation-item">
<div class="variation-info">
<span class="variation-title">${escapeHtml(varTitle)}</span>
<span class="variation-duration">${varDuration}</span>
</div>
<button class="variation-play-btn"
data-track-id="${track.id}"
data-audio-url="${v.audio_url || ''}"
data-title="${escapeHtml(varTitle)}"
data-artist="${escapeHtml(track.artist_name || '')}"
data-variation-index="${v.variation_index !== null ? v.variation_index : idx}">
<i class="fas fa-play"></i>
</button>
</div>
`;
}).join('')}
</div>
</div>
` : ''}
</div>
`;
}
// Refresh similar artists
function refreshSimilarArtists() {
// Implement refresh logic
showNotification('Similar artists refreshed!', 'success');
}
// Toggle stats view
function toggleStatsView() {
// Implement stats toggle logic
showNotification('Stats view toggled!', 'info');
}
// Contact artist
function contactArtist() {
// Redirect to contact form
window.location.href = `contact.php?artist_id=<?= $artist_id ?>`;
}
// Profile card editing functions
function editProfileCard(field, currentValue) {
console.log('đ¯ editProfileCard called for:', field);
console.log('đ¯ editProfileCard currentValue:', currentValue);
// Check if user is logged in and is the owner
const isLoggedIn = window.userIsLoggedIn || false;
const isOwner = window.userIsOwner || false;
console.log('đ Session check:', { isLoggedIn, isOwner });
if (isLoggedIn && isOwner) {
console.log('â
User can edit, showing edit modal');
console.log('đ¯ About to call showEditModal for:', field);
showEditModal(field, currentValue);
} else {
console.log('â User cannot edit, showing read-only modal');
showReadOnlyModal(field, currentValue);
}
}
function showEditModal(field, currentValue) {
console.log('đ showEditModal called with:', field, currentValue);
const fieldNames = {
'highlights': 'Artist Highlights',
'bio': 'About',
'influences': 'Influences',
'equipment': 'Equipment',
'achievements': 'Achievements',
'artist_statement': 'Artist Statement'
};
const fieldName = fieldNames[field] || field;
console.log('đ Field name resolved to:', fieldName);
// Check what type of editor to show based on field
if (field === 'highlights' || field === 'achievements') {
console.log('đ Using list editor for:', field);
showListEditModal(field, fieldName, currentValue);
} else if (field === 'influences' || field === 'equipment') {
console.log('đˇī¸ Using tag editor for:', field);
showTagEditModal(field, fieldName, currentValue);
} else if (field === 'bio') {
console.log('đ About card clicked, using simple text editor');
console.log('đ Current bio value:', currentValue);
console.log('đ Field name:', fieldName);
try {
showTextEditModal(field, fieldName, currentValue);
} catch (error) {
console.error('â Error opening bio editor:', error);
alert('Error opening bio editor: ' + error.message);
}
} else {
console.log('âī¸ Other field, calling showTextEditModal');
showTextEditModal(field, fieldName, currentValue);
}
}
function showListEditModal(field, fieldName, currentValue) {
// Parse current value as JSON array
let items = [];
try {
if (currentValue && currentValue.trim()) {
items = JSON.parse(currentValue);
}
} catch (e) {
// If parsing fails, treat as comma-separated text
if (currentValue && currentValue.trim()) {
items = currentValue.split(',').map(item => item.trim()).filter(item => item);
}
}
const modalHTML = `
<div class="profile-edit-modal" id="profileEditModal">
<div class="modal-content list-edit-modal">
<div class="modal-header">
<h3>Edit ${fieldName}</h3>
<button class="close-btn" onclick="closeEditModal()">×</button>
</div>
<div class="modal-body">
<div class="list-editor">
<div class="add-item-section">
<input type="text" id="newItemInput" placeholder="Add new ${field === 'highlights' ? 'highlight' : 'achievement'}..." class="new-item-input">
<button class="btn-add-item" onclick="addListItem('${field}')">
<i class="fas fa-plus"></i> Add
</button>
</div>
<div class="items-list" id="itemsList">
${items.map((item, index) => `
<div class="list-item" data-index="${index}">
<span class="item-text">${item}</span>
<button class="btn-remove-item" onclick="removeListItem(${index})">
<i class="fas fa-times"></i>
</button>
</div>
`).join('')}
</div>
<div class="list-help">
<p><strong>đĄ Tip:</strong> Add your ${field === 'highlights' ? 'highlights' : 'achievements'} one by one. Each item will be displayed beautifully on your profile.</p>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeEditModal()">Cancel</button>
<button class="btn-primary" onclick="saveListField('${field}')">Save Changes</button>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHTML);
// Add keyboard support for Enter key
const itemInput = document.getElementById('newItemInput');
itemInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
addListItem(field);
}
});
// Focus on input
setTimeout(() => {
itemInput.focus();
}, 100);
}
function showTagEditModal(field, fieldName, currentValue) {
console.log('đ showTagEditModal called with:', field, fieldName, currentValue);
// Parse current value as JSON array or comma-separated
let tags = [];
try {
if (currentValue && currentValue.trim()) {
tags = JSON.parse(currentValue);
}
} catch (e) {
// If parsing fails, treat as comma-separated text
if (currentValue && currentValue.trim()) {
tags = currentValue.split(',').map(tag => tag.trim()).filter(tag => tag);
}
}
// Use the EXACT same modal structure as the working list editor
const modalHTML = `
<div class="profile-edit-modal" id="profileEditModal">
<div class="modal-content list-edit-modal">
<div class="modal-header">
<h3>Edit ${fieldName}</h3>
<button class="close-btn" onclick="closeEditModal()">×</button>
</div>
<div class="modal-body">
<div class="list-editor">
<div class="add-item-section">
<input type="text" id="newItemInput" placeholder="Add new ${field === 'influences' ? 'influence' : 'equipment'}..." class="new-item-input">
<button class="btn-add-item" onclick="addListItem('${field}')">
<i class="fas fa-plus"></i> Add
</button>
</div>
<div class="items-list" id="itemsList">
${tags.map((item, index) => `
<div class="list-item" data-index="${index}">
<span class="item-text">${item}</span>
<button class="btn-remove-item" onclick="removeListItem(${index})">
<i class="fas fa-times"></i>
</button>
</div>
`).join('')}
</div>
<div class="list-help">
<p><strong>đĄ Tip:</strong> Add your ${field === 'influences' ? 'musical influences' : 'equipment'} as tags. Each tag will be displayed as a beautiful badge on your profile.</p>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeEditModal()">Cancel</button>
<button class="btn-primary" onclick="saveListField('${field}')">Save Changes</button>
</div>
</div>
</div>
`;
// Remove any existing modal first
const existingModal = document.getElementById('profileEditModal');
if (existingModal) {
existingModal.remove();
}
// Insert the new modal
document.body.insertAdjacentHTML('beforeend', modalHTML);
// Add keyboard support for Enter key
const newItemInput = document.getElementById('newItemInput');
if (newItemInput) {
newItemInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
addListItem(field);
}
});
setTimeout(() => {
newItemInput.focus();
}, 100);
}
console.log('â
Tag modal created and ready');
}
// Rating and Engagement Modal Functions
function showRatingModal() {
console.log('đ¯ showRatingModal() called!');
// Remove any existing rating modal first
const existingModal = document.getElementById('ratingModal');
if (existingModal) {
existingModal.remove();
console.log('đī¸ Removed existing rating modal');
}
const modalHTML = `
<div class="profile-edit-modal" id="ratingModal" style="z-index: 10001;">
<div class="modal-content">
<div class="modal-header">
<h3><?= htmlspecialchars(t('artist_profile.rate_modal_title'), ENT_QUOTES, 'UTF-8') ?></h3>
<button class="close-btn" onclick="closeRatingModal()">×</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_artist'), ENT_QUOTES, 'UTF-8') ?></span>
</div>
</div>
<div class="rating-comment">
<label for="ratingComment"><?= htmlspecialchars(t('artist_profile.rate_modal_comment_label'), ENT_QUOTES, 'UTF-8') ?></label>
<textarea id="ratingComment" placeholder="<?= htmlspecialchars(t('artist_profile.rate_modal_comment_placeholder_artist'), ENT_QUOTES, 'UTF-8') ?>" rows="4"></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeRatingModal()"><?= htmlspecialchars(t('artist_profile.cancel'), ENT_QUOTES, 'UTF-8') ?></button>
<button class="btn-primary" onclick="submitRating()"><?= htmlspecialchars(t('artist_profile.submit_rating'), ENT_QUOTES, 'UTF-8') ?></button>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHTML);
// Make sure modal is visible
const modal = document.getElementById('ratingModal');
if (modal) {
modal.style.display = 'flex';
modal.dataset.currentRating = '0'; // Initialize rating
console.log('â
Rating modal created and visible');
} else {
console.error('â Failed to create rating modal');
return;
}
const stars = modal.querySelectorAll('.stars-input i');
const ratingValueEl = modal.querySelector('.rating-value');
const ratingLabelEl = modal.querySelector('.rating-label');
const commentTextarea = document.getElementById('ratingComment');
let currentRating = 0;
const setStars = (rating) => {
stars.forEach((star, index) => {
if (index < rating) {
star.className = 'fas fa-star';
} else {
star.className = 'far fa-star';
}
});
if (ratingValueEl) ratingValueEl.textContent = `${rating}/10`;
if (ratingLabelEl) ratingLabelEl.textContent = rating > 0 ? getRatingLabel(rating) : 'Click stars to rate';
};
stars.forEach(star => {
star.addEventListener('click', function() {
const rating = parseInt(this.dataset.rating);
currentRating = rating;
modal.dataset.currentRating = rating.toString();
setStars(rating);
});
star.addEventListener('mouseenter', function() {
const rating = parseInt(this.dataset.rating);
setStars(rating);
});
star.addEventListener('mouseleave', function() {
setStars(currentRating);
});
});
// Fetch existing user rating so the modal reflects saved value
fetch(`/api_social.php?action=get_user_artist_rating&artist_id=<?= $artist_id ?>`)
.then(response => response.ok ? response.json() : Promise.reject(new Error('Network error')))
.then(data => {
if (data.success && data.data) {
const existingRating = parseInt(data.data.rating ?? 0);
const existingComment = data.data.comment ?? '';
// Store existing rating but keep stars unselected so user can choose a new value
if (existingRating && existingRating >= 1 && existingRating <= 10) {
modal.dataset.savedRating = existingRating.toString();
if (ratingLabelEl) {
ratingLabelEl.textContent = `Your current rating: ${existingRating}/10. Tap a star to change it.`;
}
}
if (commentTextarea && existingComment) {
commentTextarea.value = existingComment;
}
}
})
.catch(err => console.error('Failed to fetch user artist rating:', err));
}
function showTrackRatingModal(trackId, trackTitle) {
console.log('đĩ Track rating modal for:', trackId, trackTitle);
// 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'), 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'), ENT_QUOTES, 'UTF-8') ?></span>
</div>
</div>
<div class="rating-comment">
<label for="trackRatingComment"><?= htmlspecialchars(t('artist_profile.rate_modal_comment_label'), ENT_QUOTES, 'UTF-8') ?></label>
<textarea id="trackRatingComment" placeholder="<?= htmlspecialchars(t('artist_profile.rate_modal_comment_placeholder_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'), ENT_QUOTES, 'UTF-8') ?></button>
<button class="btn-primary" onclick="submitTrackRating(${trackId})"><?= htmlspecialchars(t('artist_profile.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'; // Initialize rating
console.log('â
Track rating modal created and visible');
} else {
console.error('â Failed to create track rating modal');
return;
}
// Fetch user's existing rating for this track
console.log('đĩ Fetching user rating for track:', trackId);
fetch(`/api_social.php?action=get_user_track_rating&track_id=${trackId}`)
.then(response => {
console.log('đĩ API response status:', response.status);
if (!response.ok) {
throw new Error('Network response was not ok: ' + response.status);
}
return response.json();
})
.then(data => {
console.log('đĩ API response data:', data);
let existingRating = 0;
let existingComment = '';
if (data.success && data.data) {
if (data.data.rating !== null && data.data.rating !== undefined) {
existingRating = parseInt(data.data.rating);
existingComment = data.data.comment || '';
console.log('đĩ Found existing rating:', existingRating, 'comment:', existingComment);
} else {
console.log('đĩ No existing rating found (rating is null)');
}
} else {
console.log('đĩ API returned success=false or no data:', data);
}
// Wait a tiny bit to ensure DOM is ready
setTimeout(() => {
// Initialize with existing rating if found
if (existingRating > 0) {
modal.dataset.currentRating = existingRating.toString();
// Update stars display
const stars = document.querySelectorAll('#trackRatingModal .stars-input i');
console.log('đĩ Found', stars.length, 'stars in modal');
if (stars.length === 10) {
stars.forEach((s, index) => {
if (index < existingRating) {
s.className = 'fas fa-star';
} else {
s.className = 'far fa-star';
}
});
// Update feedback
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);
}
// Set comment if exists
const commentTextarea = document.getElementById('trackRatingComment');
if (commentTextarea && existingComment) {
commentTextarea.value = existingComment;
}
console.log('đĩ Successfully loaded existing rating:', existingRating);
} else {
console.error('đĩ ERROR: Expected 10 stars but found', stars.length);
}
} else {
console.log('đĩ No existing rating to load');
}
// Add star rating functionality (after DOM is ready)
const stars = document.querySelectorAll('#trackRatingModal .stars-input i');
let currentRating = existingRating;
console.log('đĩ Setting up star event listeners for', stars.length, 'stars');
stars.forEach(star => {
star.addEventListener('click', function() {
const rating = parseInt(this.dataset.rating);
currentRating = rating;
// Store rating in modal for submission
if (modal) {
modal.dataset.currentRating = rating.toString();
}
// Update stars display
stars.forEach((s, index) => {
if (index < rating) {
s.className = 'fas fa-star';
} else {
s.className = 'far fa-star';
}
});
// Update feedback
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) => {
if (index < rating) {
s.className = 'fas fa-star';
} else {
s.className = 'far fa-star';
}
});
});
star.addEventListener('mouseleave', function() {
stars.forEach((s, index) => {
if (index < currentRating) {
s.className = 'fas fa-star';
} else {
s.className = 'far fa-star';
}
});
});
// Ensure rating card reflects latest data on load
refreshArtistRatingSummary();
});
}, 100);
})
.catch(error => {
console.error('Error fetching user rating:', error);
// Continue with empty rating if fetch fails
const stars = document.querySelectorAll('#trackRatingModal .stars-input i');
let currentRating = 0;
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) => {
if (index < rating) {
s.className = 'fas fa-star';
} else {
s.className = 'far fa-star';
}
});
document.querySelector('#trackRatingModal .rating-value').textContent = `${rating}/10`;
document.querySelector('#trackRatingModal .rating-label').textContent = getRatingLabel(rating);
});
star.addEventListener('mouseenter', function() {
const rating = parseInt(this.dataset.rating);
stars.forEach((s, index) => {
if (index < rating) {
s.className = 'fas fa-star';
} else {
s.className = 'far fa-star';
}
});
});
star.addEventListener('mouseleave', function() {
stars.forEach((s, index) => {
if (index < currentRating) {
s.className = 'fas fa-star';
} else {
s.className = 'far fa-star';
}
});
});
});
});
}
function closeTrackRatingModal() {
const modal = document.getElementById('trackRatingModal');
if (modal) modal.remove();
}
function submitTrackRating(trackId) {
// Get rating from modal data attribute or parse from text
const modal = document.getElementById('trackRatingModal');
let rating = 0;
if (modal && modal.dataset.currentRating) {
rating = parseInt(modal.dataset.currentRating, 10);
}
// Fallback: count selected stars within track modal
if (!rating || rating < 1 || rating > 10) {
const selectedStars = modal ? modal.querySelectorAll('.stars-input i.fas').length : 0;
if (selectedStars > 0) {
rating = selectedStars;
if (modal) {
modal.dataset.currentRating = rating.toString();
}
}
}
// Final fallback: parse from modal-specific rating text
if ((!rating || rating < 1 || rating > 10) && modal) {
const ratingText = modal.querySelector('.rating-value');
if (ratingText) {
const ratingStr = ratingText.textContent.split('/')[0].trim();
const parsed = parseInt(ratingStr, 10);
if (parsed >= 1 && parsed <= 10) {
rating = parsed;
if (modal) {
modal.dataset.currentRating = rating.toString();
}
}
}
}
const comment = document.getElementById('trackRatingComment')?.value || '';
// Validate rating
if (!rating || rating < 1 || rating > 10) {
showNotification('Please select a rating between 1 and 10', 'warning');
return;
}
console.log('Submitting track rating:', { trackId, rating, comment });
// Send track rating to API
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 => {
if (!response.ok) {
throw new Error('Network response was not ok: ' + response.status);
}
return response.json();
})
.then(data => {
console.log('Track rating submission response:', data);
if (data.success) {
showNotification('Track rating submitted successfully!', 'success');
closeTrackRatingModal();
// Update the track rating display
if (data.data) {
updateTrackRatingDisplay(trackId, data.data.average_rating, data.data.total_ratings);
}
} else {
const errorMsg = data.message || 'Failed to submit track rating';
console.error('Track rating submission failed:', errorMsg);
showNotification(errorMsg, 'error');
}
})
.catch(error => {
console.error('Track rating submission error:', error);
showNotification(notificationTranslations.failed_submit_track_rating + ': ' + error.message, 'error');
});
}
function updateTrackRatingDisplay(trackId, newAverageRating, newTotalRatings) {
// Update the rating text in the track card
const trackCard = document.querySelector(`[data-track-id="${trackId}"]`);
if (trackCard) {
const ratingText = trackCard.querySelector('.rating-text');
if (ratingText) {
ratingText.textContent = `${newAverageRating}/10 (${newTotalRatings})`;
}
}
}
function showEngagementModal() {
const modalHTML = `
<div class="profile-edit-modal" id="engagementModal">
<div class="modal-content">
<div class="modal-header">
<h3>Engagement Analytics</h3>
<button class="close-btn" onclick="closeEngagementModal()">×</button>
</div>
<div class="modal-body">
<div class="engagement-analytics">
<div class="analytics-item">
<div class="analytics-label">Monthly Listeners</div>
<div class="analytics-value">1.2K</div>
<div class="analytics-trend positive">+12% this month</div>
</div>
<div class="analytics-item">
<div class="analytics-label">Track Plays</div>
<div class="analytics-value">89</div>
<div class="analytics-trend positive">+8% this week</div>
</div>
<div class="analytics-item">
<div class="analytics-label">Social Shares</div>
<div class="analytics-value">156</div>
<div class="analytics-trend positive">+23% this month</div>
</div>
<div class="analytics-item">
<div class="analytics-label">Fan Interactions</div>
<div class="analytics-value">42</div>
<div class="analytics-trend neutral">No change</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn-primary" onclick="closeEngagementModal()">Close</button>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHTML);
}
function closeRatingModal() {
const modal = document.getElementById('ratingModal');
if (modal) modal.remove();
}
function closeEngagementModal() {
const modal = document.getElementById('engagementModal');
if (modal) modal.remove();
}
function submitRating() {
// Get rating from modal data attribute or parse from text
const modal = document.getElementById('ratingModal');
let rating = 0;
if (modal && modal.dataset.currentRating) {
rating = parseInt(modal.dataset.currentRating, 10);
}
// Fallback: derive rating from selected stars within the modal
if (!rating || rating < 1 || rating > 10) {
const selectedStars = modal ? modal.querySelectorAll('.stars-input i.fas').length : 0;
if (selectedStars > 0) {
rating = selectedStars;
if (modal) {
modal.dataset.currentRating = rating.toString();
}
}
}
// Final fallback: parse from the modal's rating text (scoped to modal)
if ((!rating || rating < 1 || rating > 10) && modal) {
const ratingText = modal.querySelector('.rating-value');
if (ratingText) {
const ratingStr = ratingText.textContent.split('/')[0].trim();
const parsed = parseInt(ratingStr, 10);
if (parsed >= 1 && parsed <= 10) {
rating = parsed;
if (modal) {
modal.dataset.currentRating = rating.toString();
}
}
}
}
const comment = document.getElementById('ratingComment')?.value || '';
// Validate rating
if (!rating || rating < 1 || rating > 10) {
showNotification('Please select a rating between 1 and 10', 'warning');
return;
}
// Get artist ID from the page
const artistId = <?= $artist_id ?? 0 ?>;
if (!artistId) {
showNotification('Invalid artist ID', 'error');
return;
}
console.log('Submitting rating:', { artistId, rating, comment });
// Send rating to API
const formData = new FormData();
formData.append('action', 'submit_rating');
formData.append('artist_id', artistId);
formData.append('rating', rating);
formData.append('comment', comment);
fetch('/api_social.php', {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok: ' + response.status);
}
return response.json();
})
.then(data => {
console.log('Rating submission response:', data);
if (data.success) {
showNotification('Rating submitted successfully!', 'success');
closeRatingModal();
// Update the rating display with real data from API
if (data.data) {
updateRatingDisplay(data.data.average_rating, data.data.total_ratings);
refreshArtistRatingSummary();
}
} else {
const errorMsg = data.message || 'Failed to submit rating';
console.error('Rating submission failed:', errorMsg);
showNotification(errorMsg, 'error');
}
})
.catch(error => {
console.error('Rating submission error:', error);
showNotification(notificationTranslations.failed_submit_rating + ': ' + error.message, 'error');
});
}
function updateRatingDisplay(newAverageRating, newTotalRatings) {
const ratingText = document.querySelector('.rating-card .rating-text');
const ratingCount = document.querySelector('.rating-card .rating-count');
const starsContainer = document.querySelector('.rating-card .stars-container');
const numericRating = parseFloat(newAverageRating);
const safeRating = isNaN(numericRating) ? 0 : numericRating;
if (ratingText && ratingCount && starsContainer) {
// Update average rating
ratingText.textContent = `${safeRating.toFixed(1)}/10`;
// Update vote count
ratingCount.textContent = `(${newTotalRatings} votes)`;
// Update stars display
const stars = starsContainer.querySelectorAll('i');
stars.forEach((star, index) => {
const shouldFill = index + 1 <= Math.floor(safeRating + 0.0001);
star.className = shouldFill ? 'fas fa-star' : 'far fa-star';
});
}
}
function refreshArtistRatingSummary() {
fetch(`/api_social.php?action=get_artist_rating_summary&artist_id=<?= $artist_id ?>`)
.then(response => response.ok ? response.json() : Promise.reject(new Error('Network error')))
.then(data => {
if (data.success && data.data) {
updateRatingDisplay(data.data.average_rating, data.data.total_ratings);
}
})
.catch(error => {
console.error('Failed to refresh artist rating summary:', error);
});
}
function getRatingLabel(rating) {
if (ratingLabelMap && ratingLabelMap[rating]) {
return ratingLabelMap[rating];
}
if (ratingLabelMap && ratingLabelMap['default']) {
return ratingLabelMap['default'];
}
return '<?= htmlspecialchars(t('artist_profile.rate_modal_hint_artist'), ENT_QUOTES, 'UTF-8') ?>';
}
function showBioEditModal(field, fieldName, currentValue) {
console.log('đ showBioEditModal called with:', field, fieldName, currentValue);
try {
const modalHTML = `
<div class="profile-edit-modal" id="profileEditModal">
<div class="modal-content bio-edit-modal">
<div class="modal-header">
<h3>Edit ${fieldName}</h3>
<button class="close-btn" onclick="closeEditModal()">×</button>
</div>
<div class="modal-body">
<div class="bio-editor">
<div class="bio-input-section">
<label for="bioTextarea">Tell your story:</label>
<textarea id="editField" placeholder="Share your musical journey, inspiration, and what makes you unique..." rows="8">${currentValue}</textarea>
<div class="char-count">
<span id="charCount">${(currentValue || '').length}</span> / 500 characters
</div>
</div>
<div class="bio-help">
<p><strong>đĄ Writing Tips:</strong></p>
<ul>
<li>Share your musical journey and inspiration</li>
<li>Mention your style and what makes you unique</li>
<li>Keep it personal and engaging</li>
<li>Include any notable achievements or experiences</li>
</ul>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeEditModal()">Cancel</button>
<button class="btn-primary" onclick="saveProfileField('${field}')">Save Bio</button>
</div>
</div>
</div>
`;
console.log('đ§ Creating bio modal HTML...');
document.body.insertAdjacentHTML('beforeend', modalHTML);
console.log('â
Modal added to DOM, setting up character count...');
// Add character count functionality
const textarea = document.getElementById('editField');
const charCount = document.getElementById('charCount');
if (textarea && charCount) {
textarea.addEventListener('input', function() {
const length = this.value.length;
charCount.textContent = length;
if (length > 450) {
charCount.style.color = '#f59e0b';
} else if (length > 400) {
charCount.style.color = '#10b981';
} else {
charCount.style.color = '#6b7280';
}
});
console.log('â
Character count functionality added');
setTimeout(() => {
textarea.focus();
console.log('â
Textarea focused');
}, 100);
} else {
console.error('â Textarea or charCount not found');
}
} catch (error) {
console.error('â Error in showBioEditModal:', error);
alert('Error creating bio modal: ' + error.message);
}
}
function showTextEditModal(field, fieldName, currentValue) {
console.log('đ showTextEditModal called with:', field, fieldName, currentValue);
try {
const modalHTML = `
<div class="profile-edit-modal" id="profileEditModal">
<div class="modal-content">
<div class="modal-header">
<h3>Edit ${fieldName}</h3>
<button class="close-btn" onclick="closeEditModal()">×</button>
</div>
<div class="modal-body">
<textarea id="editField" placeholder="Enter your ${fieldName.toLowerCase()}..." rows="6">${currentValue || ''}</textarea>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeEditModal()">Cancel</button>
<button class="btn-primary" onclick="saveProfileField('${field}')">Save</button>
</div>
</div>
</div>
`;
console.log('đ§ Creating text modal HTML...');
document.body.insertAdjacentHTML('beforeend', modalHTML);
const modal = document.getElementById('profileEditModal');
console.log('â
Modal created:', modal);
if (modal) {
const textarea = document.getElementById('editField');
console.log('â
Textarea found:', textarea);
console.log('â
Textarea value:', textarea.value);
setTimeout(() => {
textarea.focus();
console.log('â
Textarea focused');
}, 100);
} else {
console.error('â Modal not found after creation');
}
} catch (error) {
console.error('â Error in showTextEditModal:', error);
alert('Error creating text modal: ' + error.message);
}
}
function showReadOnlyModal(field, currentValue) {
const fieldNames = {
'highlights': 'Artist Highlights',
'bio': 'About',
'influences': 'Influences',
'equipment': 'Equipment',
'achievements': 'Achievements',
'artist_statement': 'Artist Statement'
};
const fieldName = fieldNames[field] || field;
const displayValue = currentValue || 'No information available yet.';
const modalHTML = `
<div class="profile-edit-modal" id="profileEditModal">
<div class="modal-content">
<div class="modal-header">
<h3>${fieldName}</h3>
<button class="close-btn" onclick="closeEditModal()">×</button>
</div>
<div class="modal-body">
<div class="read-only-content">${displayValue}</div>
</div>
<div class="modal-footer">
<button class="btn-primary" onclick="closeEditModal()">Close</button>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHTML);
}
function closeEditModal() {
const modal = document.getElementById('profileEditModal');
if (modal) {
modal.remove();
}
}
// List editing functions
function addListItem(field) {
const input = document.getElementById('newItemInput');
const itemText = input.value.trim();
if (!itemText) {
showNotification('Please enter some text first', 'warning');
return;
}
const itemsList = document.getElementById('itemsList');
const newIndex = itemsList.children.length;
const newItem = document.createElement('div');
newItem.className = 'list-item';
newItem.setAttribute('data-index', newIndex);
newItem.innerHTML = `
<span class="item-text">${itemText}</span>
<button class="btn-remove-item" onclick="removeListItem(${newIndex})">
<i class="fas fa-times"></i>
</button>
`;
itemsList.appendChild(newItem);
input.value = '';
input.focus();
// Update all indices
updateItemIndices();
}
function removeListItem(index) {
const itemsList = document.getElementById('itemsList');
const items = itemsList.querySelectorAll('.list-item');
if (index < items.length) {
items[index].remove();
updateItemIndices();
}
}
function updateItemIndices() {
const items = document.querySelectorAll('.list-item');
items.forEach((item, newIndex) => {
item.setAttribute('data-index', newIndex);
const removeBtn = item.querySelector('.btn-remove-item');
removeBtn.onclick = () => removeListItem(newIndex);
});
}
function saveListField(field) {
const items = Array.from(document.querySelectorAll('.list-item .item-text')).map(span => span.textContent.trim());
// Convert to JSON string for storage
const jsonValue = JSON.stringify(items);
// Save using the existing save function
saveProfileField(field, jsonValue);
}
// Tag editing functions
function addTag(field) {
const input = document.getElementById('newTagInput');
const tagText = input.value.trim();
if (!tagText) {
showNotification('Please enter some text first', 'warning');
return;
}
const tagsContainer = document.getElementById('tagsContainer');
const newIndex = tagsContainer.children.length;
const newTag = document.createElement('div');
newTag.className = 'tag-item';
newTag.setAttribute('data-index', newIndex);
newTag.innerHTML = `
<span class="tag-text">${tagText}</span>
<button class="btn-remove-tag" onclick="removeTag(${newIndex})">
<i class="fas fa-times"></i>
</button>
`;
tagsContainer.appendChild(newTag);
input.value = '';
input.focus();
// Update all indices
updateTagIndices();
}
function removeTag(index) {
const tagsContainer = document.getElementById('tagsContainer');
const tags = tagsContainer.querySelectorAll('.tag-item');
if (index < tags.length) {
tags[index].remove();
updateTagIndices();
}
}
function updateTagIndices() {
const tags = document.querySelectorAll('.tag-item');
tags.forEach((tag, newIndex) => {
tag.setAttribute('data-index', newIndex);
const removeBtn = tag.querySelector('.btn-remove-tag');
removeBtn.onclick = () => removeTag(newIndex);
// Update the onchange handler for the input
const input = tag.querySelector('.tag-text-input');
if (input) {
input.onchange = (e) => updateTagText(newIndex, e.target.value);
}
});
}
function updateTagText(index, newText) {
const tags = document.querySelectorAll('.tag-item');
if (index < tags.length) {
const tag = tags[index];
const input = tag.querySelector('.tag-text-input');
if (input) {
input.value = newText.trim();
console.log(`â
Tag ${index} updated to: ${newText}`);
}
}
}
function saveTagField(field) {
const tags = Array.from(document.querySelectorAll('.tag-item .tag-text-input')).map(input => input.value.trim());
// Convert to JSON string for storage
const jsonValue = JSON.stringify(tags);
// Save using the existing save function
saveProfileField(field, jsonValue);
}
function saveProfileField(field, value = null) {
if (value === null) {
value = document.getElementById('editField').value.trim();
}
// Show loading state
const saveBtn = document.querySelector('.modal-footer .btn-primary');
const originalText = saveBtn.textContent;
saveBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Saving...';
saveBtn.disabled = true;
// Create form data for the API
const formData = new FormData();
formData.append('action', 'update_profile');
formData.append('field', field);
formData.append('value', value);
// Send update to server using the working api_social.php endpoint
fetch('/api_social.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification('Profile updated successfully!', 'success');
// Close modal
closeEditModal();
// Reload page to show updated content
setTimeout(() => {
window.location.reload();
}, 1000);
} else {
showNotification(data.message || notificationTranslations.failed_update_profile, 'error');
}
})
.catch(error => {
console.error('Profile update error:', error);
showNotification(notificationTranslations.failed_update_profile_retry, 'error');
})
.finally(() => {
// Reset button state
saveBtn.innerHTML = originalText;
saveBtn.disabled = false;
});
}
function updateProfileCard(field, value) {
const card = document.querySelector(`.${field}-card`);
if (card) {
const content = card.querySelector('.card-content');
if (value) {
content.innerHTML = `<div class="card-text">${value}</div>`;
} else {
const fieldNames = {
'highlights': 'highlights',
'bio': 'bio',
'influences': 'influences',
'equipment': 'equipment',
'achievements': 'achievements',
'artist_statement': 'artist statement'
};
const fieldName = fieldNames[field] || field;
content.innerHTML = `
<div class="placeholder-content">
<div class="placeholder-icon">âī¸</div>
<p>No ${fieldName} yet. Click to add!</p>
</div>
`;
}
}
}
// Play artist radio - loads artist's tracks as a playlist in global player
async function playArtistRadio() {
const artistId = <?= $artist_id ?>;
const artistName = '<?= htmlspecialchars($artist['username'] ?? 'Artist', ENT_QUOTES, 'UTF-8') ?>';
if (typeof window.showNotification === 'function') {
window.showNotification('đĩ Loading ' + artistName + ' Radio...', 'info');
}
// Check if global player is available
if (!window.enhancedGlobalPlayer) {
if (typeof window.showNotification === 'function') {
window.showNotification('Audio player not ready. Please wait...', 'error');
}
return;
}
try {
// Fetch artist's tracks
const response = await fetch(`/api/get_artist_tracks.php?artist_id=${artistId}&type=completed&_t=${Date.now()}`);
const data = await response.json();
if (data.success && data.tracks && data.tracks.length > 0) {
// Process tracks to use signed_audio_url which respects variation selection
const processedTracks = data.tracks.map(track => ({
...track,
audio_url: track.signed_audio_url || track.audio_url // Use signed URL that respects variation selection
}));
// Load tracks into global player as a playlist
if (window.enhancedGlobalPlayer.loadArtistPlaylist) {
// Use custom artist playlist loader if available
window.enhancedGlobalPlayer.loadArtistPlaylist(processedTracks, artistName, true);
} else {
// Fallback: manually set playlist and play first track
window.enhancedGlobalPlayer.currentPlaylist = processedTracks;
window.enhancedGlobalPlayer.currentPlaylistType = 'artist_' + artistId;
window.enhancedGlobalPlayer.currentTrackIndex = 0;
// Play first track
const firstTrack = processedTracks[0];
window.enhancedGlobalPlayer.playTrack(
firstTrack.audio_url,
firstTrack.title,
firstTrack.artist_name || artistName,
firstTrack.id,
firstTrack.user_id
);
}
if (typeof window.showNotification === 'function') {
window.showNotification('đĩ Now playing: ' + artistName + ' Radio (' + data.tracks.length + ' tracks)', 'success');
}
} else {
if (typeof window.showNotification === 'function') {
window.showNotification('No tracks available for this artist', 'error');
}
}
} catch (error) {
console.error('Error loading artist radio:', error);
if (typeof window.showNotification === 'function') {
window.showNotification(notificationTranslations.failed_load_artist_radio, 'error');
}
}
}
// Ranking Modal Functions
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')) ?>',
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')) ?>',
close: '<?= addslashes(t('track.rankings.breakdown.close')) ?>',
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')) ?>',
track: '<?= addslashes(t('track.rankings.breakdown.track')) ?>',
showing: '<?= addslashes(t('track.rankings.breakdown.showing')) ?>',
no_tracks: '<?= addslashes(t('track.rankings.breakdown.no_tracks')) ?>',
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')) ?>',
// Artist ranking translations
artist_ranking: '<?= addslashes(t('track.rankings.breakdown.artist_ranking')) ?>',
view_all_artist_rankings: '<?= addslashes(t('track.rankings.breakdown.view_all_artist_rankings')) ?>',
all_artist_rankings_title: '<?= addslashes(t('track.rankings.breakdown.all_artist_rankings_title')) ?>',
artist: '<?= addslashes(t('track.rankings.breakdown.artist')) ?>',
artists: '<?= addslashes(t('track.rankings.breakdown.artists')) ?>',
no_artists: '<?= addslashes(t('track.rankings.breakdown.no_artists')) ?>',
artist_rating: '<?= addslashes(t('track.rankings.breakdown.artist_rating')) ?>'
};
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 openArtistRankingModal(artistId, rankings, artist) {
console.log('đ openArtistRankingModal called for artist:', artistId);
console.log('đ Rankings data:', rankings);
console.log('đ¤ Artist data:', artist);
if (!rankings || !artist) {
console.error('â Missing rankings or artist data');
alert('Ranking data is not available for this artist.');
return;
}
try {
showArtistRankingBreakdown(rankings, artist);
} catch (error) {
console.error('â Error opening artist ranking breakdown:', error);
alert('Error opening artist 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 || 'Track'}</h3>
<button class="close-btn" onclick="closeRankingBreakdownModal()">×</button>
</div>
<div class="modal-body" style="overflow-y: auto; flex: 1; padding: ${isMobile ? '1rem' : '2rem'}; -webkit-overflow-scrolling: touch;">
<div style="margin-bottom: 2rem;">
<h4 style="color: #667eea; margin-bottom: 1rem;">${t.overall_ranking || 'Overall Ranking'}</h4>
<div style="background: rgba(0, 0, 0, 0.2); padding: 1rem; border-radius: 8px; margin-bottom: 1rem;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;">
<span style="color: #a0aec0;">${t.rank || ''}:</span>
<span style="font-size: 1.5rem; font-weight: 700; color: #fbbf24;">#${rankings.overall}</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="color: #a0aec0;">${t.out_of || 'Out of'}:</span>
<span style="color: #ffffff;">${rankings.total_tracks} ${t.tracks || 'tracks'}</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 0.5rem;">
<span style="color: #a0aec0;">${t.percentile || 'Percentile'}:</span>
<span style="color: #ffffff;">Top ${rankings.overall_percentile}%</span>
</div>
</div>
</div>
<div style="margin-bottom: 2rem;">
<h4 style="color: #667eea; margin-bottom: 1rem;">${t.score_calculation || 'Score Calculation'}</h4>
<div style="background: rgba(0, 0, 0, 0.2); padding: 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;">
<span style="color: #a0aec0;">${t.plays || 'Plays'} (${plays} Ã 1.0):</span>
<span style="color: #ffffff; font-weight: 600;">${breakdown.plays_score || 0}</span>
</div>
<small style="color: #888; font-size: 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;">
<span style="color: #a0aec0;">${t.likes || 'Likes'} (${likes} Ã 2.0):</span>
<span style="color: #ffffff; font-weight: 600;">${breakdown.likes_score || 0}</span>
</div>
<small style="color: #888; font-size: 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;">
<span style="color: #a0aec0;">${t.track_votes || 'Track Votes'} (${votes} Ã 3.0):</span>
<span style="color: #ffffff; font-weight: 600;">${breakdown.votes_score || 0}</span>
</div>
<small style="color: #888; font-size: 0.85rem;">${votes} ${t.track_votes_label || 'track votes'} (thumbs up/down on track) Ã ${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;">
<span style="color: #a0aec0;">${t.track_rating || 'Track Rating'} (${(Number(avgRating) || 0).toFixed(1)} Ã ${(ratingCount || 0)} Ã 5.0):</span>
<span style="color: #ffffff; font-weight: 600;">${breakdown.rating_score || 0}</span>
</div>
<small style="color: #888; font-size: 0.85rem;">${(Number(avgRating) || 0).toFixed(1)} ${t.avg_rating || 'avg rating'} Ã ${(ratingCount || 0)} ${t.track_ratings || 'track ratings'} (1-10 stars on track) Ã ${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;">
<span style="color: #fbbf24; font-weight: 700; font-size: 1.1rem;">${t.total_score || 'Total Score'}:</span>
<span style="color: #fbbf24; font-weight: 700; font-size: 1.3rem;">${breakdown.total_score || 0}</span>
</div>
</div>
</div>
</div>
<div style="margin-bottom: 2rem;">
<h4 style="color: #667eea; margin-bottom: 1rem;">${t.individual_rankings || 'Individual Rankings'}</h4>
<div style="display: grid; gap: 0.75rem;">
<div style="background: rgba(0, 0, 0, 0.2); padding: 0.75rem; border-radius: 8px; display: flex; justify-content: space-between;">
<span style="color: #a0aec0;">${t.by_plays || 'By Plays'}:</span>
<span style="color: #ffffff; font-weight: 600;">#${rankings.plays} ${t.of || 'of'} ${rankings.total_tracks}</span>
</div>
<div style="background: rgba(0, 0, 0, 0.2); padding: 0.75rem; border-radius: 8px; display: flex; justify-content: space-between;">
<span style="color: #a0aec0;">${t.by_likes || 'By Likes'}:</span>
<span style="color: #ffffff; font-weight: 600;">#${rankings.likes} ${t.of || 'of'} ${rankings.total_tracks}</span>
</div>
${ratingCount >= 3 && rankings.rating ? `
<div style="background: rgba(0, 0, 0, 0.2); padding: 0.75rem; border-radius: 8px; display: flex; justify-content: space-between;">
<span style="color: #a0aec0;">${t.by_rating || 'By Rating'}:</span>
<span style="color: #ffffff; font-weight: 600;">#${rankings.rating}</span>
</div>
` : ''}
</div>
</div>
<div style="background: rgba(102, 126, 234, 0.1); padding: 1rem; border-radius: 8px; border-left: 3px solid #667eea;">
<small style="color: #a0aec0; font-size: 0.85rem;">
<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: 2rem; padding-top: 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'};">${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 showArtistRankingBreakdown(rankings, artist) {
console.log('đ showArtistRankingBreakdown called with:', rankings, artist);
if (!rankings || !artist) {
console.error('â Missing rankings or artist data');
alert('Missing data to display artist ranking breakdown.');
return;
}
console.log('â
Data validated, creating modal...');
// Remove any existing modal
const existingModal = document.getElementById('rankingBreakdownModal');
if (existingModal) {
existingModal.remove();
}
// 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(artist.total_plays) || 0;
const likes = parseInt(artist.total_likes) || 0;
const avgRating = parseFloat(artist.average_rating) || 0;
const ratingCount = parseInt(artist.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.artist_ranking || 'Artist Ranking'}: ${artist.username || 'Artist'}</h3>
<button class="close-btn" onclick="closeRankingBreakdownModal()">×</button>
</div>
<div class="modal-body" style="overflow-y: auto; flex: 1; padding: ${isMobile ? '1rem' : '2rem'}; -webkit-overflow-scrolling: touch;">
<div style="margin-bottom: 2rem;">
<h4 style="color: #667eea; margin-bottom: 1rem;">${t.overall_ranking || 'Overall Ranking'}</h4>
<div style="background: rgba(0, 0, 0, 0.2); padding: 1rem; border-radius: 8px; margin-bottom: 1rem;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;">
<span style="color: #a0aec0;">${t.rank || ''}:</span>
<span style="font-size: 1.5rem; font-weight: 700; color: #fbbf24;">#${rankings.overall}</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="color: #a0aec0;">${t.out_of || 'Out of'}:</span>
<span style="color: #ffffff;">${rankings.total_artists} ${t.artists || 'artists'}</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 0.5rem;">
<span style="color: #a0aec0;">${t.percentile || 'Percentile'}:</span>
<span style="color: #ffffff;">Top ${rankings.overall_percentile}%</span>
</div>
</div>
</div>
<div style="margin-bottom: 2rem;">
<h4 style="color: #667eea; margin-bottom: 1rem;">${t.score_calculation || 'Score Calculation'}</h4>
<div style="background: rgba(0, 0, 0, 0.2); padding: 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;">
<span style="color: #a0aec0;">${t.plays || 'Plays'} (${plays} Ã 1.0):</span>
<span style="color: #ffffff; font-weight: 600;">${breakdown.plays_score || 0}</span>
</div>
<small style="color: #888; font-size: 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;">
<span style="color: #a0aec0;">${t.likes || 'Likes'} (${likes} Ã 2.0):</span>
<span style="color: #ffffff; font-weight: 600;">${breakdown.likes_score || 0}</span>
</div>
<small style="color: #888; font-size: 0.85rem;">${likes} ${t.likes || 'likes'} Ã ${t.weight || 'weight'} 2.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;">
<span style="color: #a0aec0;">${t.artist_rating || 'Artist Rating'} (${(Number(avgRating) || 0).toFixed(1)} Ã ${(ratingCount || 0)} Ã 5.0):</span>
<span style="color: #ffffff; font-weight: 600;">${breakdown.rating_score || 0}</span>
</div>
<small style="color: #888; font-size: 0.85rem;">${(Number(avgRating) || 0).toFixed(1)} ${t.avg_rating || 'avg rating'} Ã ${(ratingCount || 0)} ${t.ratings || 'ratings'} (1-10 stars on artist) Ã ${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;">
<span style="color: #fbbf24; font-weight: 700; font-size: 1.1rem;">${t.total_score || 'Total Score'}:</span>
<span style="color: #fbbf24; font-weight: 700; font-size: 1.3rem;">${breakdown.total_score || 0}</span>
</div>
</div>
</div>
</div>
<div style="margin-top: 2rem; padding-top: 2rem; padding-bottom: 1rem; border-top: 2px solid rgba(255, 255, 255, 0.1);">
<button class="btn-primary" onclick="showAllArtistRankings()" 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_artist_rankings || 'View All Artist 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'};">${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.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);
}
// Refresh ranking modal with updated vote count
function refreshRankingModal(trackId) {
if (!trackId) {
console.warn('â ī¸ Cannot refresh modal: no track ID');
return;
}
const modal = document.getElementById('rankingBreakdownModal');
if (!modal) {
console.log('âšī¸ Modal not open, no need to refresh');
return;
}
console.log('đ Updating ranking modal with new vote count...');
// Get the current track data from the card
const card = document.querySelector(`[data-track-id="${trackId}"]`);
if (!card) {
console.warn('â ī¸ Card not found for track:', trackId);
return;
}
// Get updated vote count from the card
const voteCountElement = card.querySelector('.vote-count-card');
if (voteCountElement) {
const newVoteCount = parseInt(voteCountElement.textContent.replace(/,/g, '')) || 0;
// Update vote count in modal
const modalVoteSection = modal.querySelector('[style*="Votes"]');
if (modalVoteSection) {
// Find the vote count display and update it
const voteText = modalVoteSection.textContent;
const voteMatch = voteText.match(/Votes.*?\((\d+)\s*Ã\s*3\.0\)/);
if (voteMatch) {
// Update the vote count in the modal
const votesScore = newVoteCount * 3.0;
const newVoteText = voteText.replace(/Votes.*?\(\d+\s*Ã\s*3\.0\)/, `Votes (${newVoteCount} Ã 3.0)`);
// Also update the score
const scoreElement = modalVoteSection.querySelector('span[style*="font-weight: 600"]');
if (scoreElement) {
scoreElement.textContent = votesScore.toFixed(1);
}
}
}
// Recalculate total score
updateModalTotalScore(modal, newVoteCount);
console.log('â
Ranking modal updated with new vote count:', newVoteCount);
}
}
// Update total score in modal
function updateModalTotalScore(modal, newVoteCount) {
// Get all score values
const playsScoreEl = modal.querySelector('[style*="Plays"]')?.nextElementSibling?.querySelector('span[style*="font-weight: 600"]');
const likesScoreEl = modal.querySelector('[style*="Likes"]')?.nextElementSibling?.querySelector('span[style*="font-weight: 600"]');
const votesScoreEl = modal.querySelector('[style*="Votes"]')?.nextElementSibling?.querySelector('span[style*="font-weight: 600"]');
const ratingScoreEl = modal.querySelector('[style*="Rating"]')?.nextElementSibling?.querySelector('span[style*="font-weight: 600"]');
const totalScoreEl = modal.querySelector('[style*="Total Score"]')?.nextElementSibling;
if (totalScoreEl) {
const playsScore = parseFloat(playsScoreEl?.textContent || 0);
const likesScore = parseFloat(likesScoreEl?.textContent || 0);
const votesScore = newVoteCount * 3.0;
const ratingScore = parseFloat(ratingScoreEl?.textContent || 0);
const totalScore = playsScore + likesScore + votesScore + ratingScore;
totalScoreEl.textContent = totalScore.toFixed(1);
// Update votes score display
if (votesScoreEl) {
votesScoreEl.textContent = votesScore.toFixed(1);
}
}
}
function showAllTrackRankings() {
// Close the current modal
closeRankingBreakdownModal();
// Create loading modal
const loadingModal = `
<div class="profile-edit-modal" id="allRankingsModal" style="z-index: 10002;">
<div class="modal-content" style="max-width: 900px; max-height: 90vh;">
<div class="modal-header">
<h3>đ ${window.rankingTranslations?.all_rankings_title || 'All Track Rankings'}</h3>
<button class="close-btn" onclick="closeAllRankingsModal()">×</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>${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 currentTrackId = window.currentTrack?.id;
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 {
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 isCurrentTrack = track.id == currentTrackId;
const rowStyle = isCurrentTrack
? 'background: rgba(251, 191, 36, 0.2); border-left: 3px solid #fbbf24;'
: 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: ${isCurrentTrack ? '700' : '600'}; color: ${isCurrentTrack ? '#fbbf24' : '#ffffff'};">
#${track.rank}
</td>
<td style="padding: 12px;">
<a href="/track.php?id=${track.id}" style="color: ${isCurrentTrack ? '#fbbf24' : '#667eea'}; text-decoration: none; font-weight: ${isCurrentTrack ? '700' : '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: ${isCurrentTrack ? '#fbbf24' : '#ffffff'};">
${track.total_score.toLocaleString()}
</td>
</tr>
`;
}).join('')}
</tbody>
</table>
</div>
${pagination && pagination.total_tracks > pagination.per_page ? `
<div style="margin-top: 1rem; padding-top: 1rem; border-top: 1px solid rgba(255, 255, 255, 0.1); text-align: center; color: #a0aec0;">
${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 showAllArtistRankings() {
// 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>đ ${window.rankingTranslations?.all_artist_rankings_title || 'All Artist Rankings'}</h3>
<button class="close-btn" onclick="closeAllRankingsModal()">×</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>${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 artist rankings
fetch('/api/get_all_artist_rankings.php?per_page=500')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.text();
})
.then(text => {
console.log('Raw API response:', text);
try {
const data = JSON.parse(text);
if (data.success && data.artists) {
displayAllArtistRankings(data.artists, data.pagination);
} else {
console.error('API returned error:', data);
if (typeof showNotification === 'function') {
showNotification(data.error || 'Failed to load artist rankings', 'error');
} else {
alert(data.error || 'Failed to load artist rankings');
}
closeAllRankingsModal();
}
} catch (parseError) {
console.error('Failed to parse JSON:', parseError, 'Response:', text);
if (typeof showNotification === 'function') {
showNotification('Invalid response from server. Please try again.', 'error');
} else {
alert('Invalid response from server. Please try again.');
}
closeAllRankingsModal();
}
})
.catch(error => {
console.error('Error fetching artist rankings:', error);
if (typeof showNotification === 'function') {
showNotification('Failed to load artist rankings. Please try again.', 'error');
} else {
alert('Failed to load artist rankings. Please try again.');
}
closeAllRankingsModal();
});
}
function displayAllArtistRankings(artists, pagination) {
const modal = document.getElementById('allRankingsModal');
if (!modal) return;
const t = window.rankingTranslations || {};
const currentArtistId = <?= $artist_id ?>;
// Detect mobile device
const isMobile = window.innerWidth <= 768;
// Update modal width for mobile
const modalContent = modal.querySelector('.modal-content');
if (modalContent) {
if (isMobile) {
modalContent.style.maxWidth = '95%';
modalContent.style.width = '95%';
modalContent.style.maxHeight = '90vh';
} else {
modalContent.style.maxWidth = '900px';
}
}
let artistsHTML = '';
if (artists.length === 0) {
artistsHTML = `
<div style="text-align: center; padding: 3rem; color: #a0aec0;">
<p>${t.no_artists || 'No artists found'}</p>
</div>
`;
} else if (isMobile) {
// Mobile: Card-based layout
artistsHTML = `
<div style="overflow-y: auto; max-height: 60vh; padding: 0.5rem;">
${artists.map((artist, index) => {
const isCurrentArtist = artist.id == currentArtistId;
return `
<div style="background: ${isCurrentArtist ? 'rgba(251, 191, 36, 0.15)' : 'rgba(255, 255, 255, 0.05)'}; border: 1px solid ${isCurrentArtist ? 'rgba(251, 191, 36, 0.3)' : 'rgba(255, 255, 255, 0.1)'}; border-radius: 12px; padding: 1rem; margin-bottom: 0.75rem;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem;">
<div style="display: flex; align-items: center; gap: 0.75rem;">
<div style="font-weight: ${isCurrentArtist ? '700' : '600'}; color: ${isCurrentArtist ? '#fbbf24' : '#ffffff'}; font-size: 1.1rem;">
#${artist.rank || (index + 1)}
</div>
<div>
<a href="/artist_profile.php?id=${artist.id}" style="color: ${isCurrentArtist ? '#fbbf24' : '#667eea'}; text-decoration: none; font-weight: ${isCurrentArtist ? '700' : '600'}; font-size: 0.95rem;" onclick="event.stopPropagation(); loadArtistProfile(${artist.id}); return false;">
${escapeHtmlForModal(artist.username || 'Unknown Artist')}
</a>
</div>
</div>
<div style="font-weight: 600; color: ${isCurrentArtist ? '#fbbf24' : '#ffffff'}; font-size: 0.9rem;">
${parseFloat(artist.total_score || 0).toFixed(1)}
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.5rem; font-size: 0.8rem;">
<div style="text-align: center; color: #a0aec0;">
<div style="font-weight: 600; color: #667eea; margin-bottom: 0.25rem;">${t.plays || 'Plays'}</div>
<div>${(artist.total_plays || 0).toLocaleString()}</div>
</div>
<div style="text-align: center; color: #a0aec0;">
<div style="font-weight: 600; color: #667eea; margin-bottom: 0.25rem;">${t.likes || 'Likes'}</div>
<div>${(artist.total_likes || 0).toLocaleString()}</div>
</div>
<div style="text-align: center; color: #a0aec0;">
<div style="font-weight: 600; color: #667eea; margin-bottom: 0.25rem;">${t.rating || 'Rating'}</div>
<div>${(artist.rating_count || 0) > 0 ? (parseFloat(artist.average_rating || 0).toFixed(1) + ' (' + artist.rating_count + ')') : '-'}</div>
</div>
</div>
</div>
`;
}).join('')}
</div>
${pagination && pagination.total_artists > pagination.per_page ? `
<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: 0.85rem;">
${t.showing || 'Showing'} ${((pagination.page - 1) * pagination.per_page) + 1}-${Math.min(pagination.page * pagination.per_page, pagination.total_artists)} ${t.of || 'of'} ${pagination.total_artists.toLocaleString()} ${t.artists || 'artists'}
</div>
` : ''}
`;
} else {
// Desktop: Table layout
artistsHTML = `
<div style="overflow-x: auto; max-height: 60vh;">
<table style="width: 100%; border-collapse: collapse; color: white; min-width: 600px;">
<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.artist || 'Artist'}</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>
${artists.map((artist, index) => {
const isCurrentArtist = artist.id == currentArtistId;
return `
<tr style="border-bottom: 1px solid rgba(255, 255, 255, 0.1); ${isCurrentArtist ? 'background: rgba(251, 191, 36, 0.1);' : ''}">
<td style="padding: 12px; font-weight: ${isCurrentArtist ? '700' : '600'}; color: ${isCurrentArtist ? '#fbbf24' : '#ffffff'};">
#${artist.rank || (index + 1)}
</td>
<td style="padding: 12px;">
<a href="/artist_profile.php?id=${artist.id}" style="color: ${isCurrentArtist ? '#fbbf24' : '#667eea'}; text-decoration: none; font-weight: ${isCurrentArtist ? '700' : '600'};" onclick="event.stopPropagation(); loadArtistProfile(${artist.id}); return false;">
${escapeHtmlForModal(artist.username || 'Unknown Artist')}
</a>
</td>
<td style="padding: 12px; text-align: center; color: #a0aec0;">
${(artist.total_plays || 0).toLocaleString()}
</td>
<td style="padding: 12px; text-align: center; color: #a0aec0;">
${(artist.total_likes || 0).toLocaleString()}
</td>
<td style="padding: 12px; text-align: center; color: #a0aec0;">
${(artist.rating_count || 0) > 0 ? (parseFloat(artist.average_rating || 0).toFixed(1) + ' (' + artist.rating_count + ')') : '-'}
</td>
<td style="padding: 12px; text-align: right; font-weight: 600; color: ${isCurrentArtist ? '#fbbf24' : '#ffffff'};">
${parseFloat(artist.total_score || 0).toFixed(1)}
</td>
</tr>
`;
}).join('')}
</tbody>
</table>
</div>
${pagination && pagination.total_artists > pagination.per_page ? `
<div style="margin-top: 1rem; padding-top: 1rem; border-top: 1px solid rgba(255, 255, 255, 0.1); text-align: center; color: #a0aec0;">
${t.showing || 'Showing'} ${((pagination.page - 1) * pagination.per_page) + 1}-${Math.min(pagination.page * pagination.per_page, pagination.total_artists)} ${t.of || 'of'} ${pagination.total_artists.toLocaleString()} ${t.artists || 'artists'}
</div>
` : ''}
`;
}
modal.querySelector('.modal-body').innerHTML = artistsHTML;
// Add click outside to close
modal.addEventListener('click', function(e) {
if (e.target === modal) {
closeAllRankingsModal();
}
});
}
function escapeHtmlForModal(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>
<?php if (isset($_SESSION['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) {
console.error('Add to Crate modal not found');
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;
currentTrackTitleForCrate = 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">${escapeHtmlForModal(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 adding track to crate:', error);
if (typeof showNotification === 'function') {
showNotification('Failed to add track to crate', 'error');
}
loadUserCratesForModal();
});
}
// Close modal on escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeAddToCrateModal();
}
});
// Close modal on background click
document.getElementById('addToCrateModal')?.addEventListener('click', function(e) {
if (e.target === this) {
closeAddToCrateModal();
}
});
</script>
<?php endif; ?>
<?php
// Close the page
if ($is_ajax) {
echo '</div>';
} else {
include 'includes/footer.php';
}
?>