![]() Server : Apache/2 System : Linux server-15-235-50-60 5.15.0-164-generic #174-Ubuntu SMP Fri Nov 14 20:25:16 UTC 2025 x86_64 User : gositeme ( 1004) PHP Version : 8.2.29 Disable Function : exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname Directory : /home/gositeme/domains/soundstudiopro.com/private_html/ |
<?php
session_start();
// Include translation system
require_once 'includes/translations.php';
require_once 'config/database.php';
// Include audio token system for signed URLs
require_once 'utils/audio_token.php';
// Prevent caching of this page to ensure deleted tracks don't show
header('Cache-Control: no-cache, no-store, must-revalidate, max-age=0');
header('Pragma: no-cache');
header('Expires: 0');
// Set page variables
$current_page = 'community_fixed';
$page_title = t('community.page_title');
$page_description = t('community.page_description');
// Include header
include 'includes/header.php';
$pdo = getDBConnection();
// Ensure wishlist table exists for queries on this page
try {
$pdo->exec("
CREATE TABLE IF NOT EXISTS user_wishlist (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
track_id INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY unique_wishlist (user_id, track_id),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (track_id) REFERENCES music_tracks(id) ON DELETE CASCADE
)
");
} catch (Exception $e) {
error_log('Wishlist ensure failed: ' . $e->getMessage());
}
// Get current user info
$user_id = $_SESSION['user_id'] ?? null;
$user_name = 'Guest';
if ($user_id) {
$stmt = $pdo->prepare("SELECT name FROM users WHERE id = ?");
$stmt->execute([$user_id]);
$user = $stmt->fetch();
$user_name = $user['name'] ?? 'User';
}
// Get filter parameters
$sort = $_GET['sort'] ?? 'latest'; // latest, trending, popular, random
$genre = $_GET['genre'] ?? 'all';
// Decode and clean genre parameter (remove quotes if present, decode URL encoding)
if ($genre !== 'all') {
$genre = urldecode($genre);
$genre = trim($genre, '"\'');
$genre = trim($genre);
}
$time_filter = $_GET['time'] ?? 'all'; // all, today, week, month
$search = $_GET['search'] ?? ''; // search keyword
// Decode and clean search parameter (remove quotes if present, decode URL encoding)
if (!empty($search)) {
$search = urldecode($search);
$search = trim($search, '"\'');
$search = trim($search);
}
$page = max(1, intval($_GET['page'] ?? 1));
$per_page = 24; // Efficient grid layout
$offset = ($page - 1) * $per_page;
// Build time filter
$time_condition = '';
switch ($time_filter) {
case 'today':
$time_condition = 'AND mt.created_at >= CURDATE()';
break;
case 'week':
$time_condition = 'AND mt.created_at >= DATE_SUB(NOW(), INTERVAL 1 WEEK)';
break;
case 'month':
$time_condition = 'AND mt.created_at >= DATE_SUB(NOW(), INTERVAL 1 MONTH)';
break;
}
// Build sort order
$order_by = 'mt.created_at DESC';
switch ($sort) {
case 'trending':
$order_by = '(mt.likes_count * 2 + mt.plays_count + mt.comments_count) DESC, mt.created_at DESC';
break;
case 'popular':
$order_by = 'mt.likes_count DESC, mt.plays_count DESC';
break;
case 'random':
$order_by = 'RAND()';
break;
default:
$order_by = 'mt.created_at DESC';
}
// Build genre filter - check genre column, tags, and metadata JSON
// Use case-insensitive LIKE for all conditions to match partial words (e.g., "deep" in "Deep House")
$genre_condition = '';
if ($genre !== 'all' && !empty($genre)) {
// Normalize genre parameter to lowercase for consistent matching
$genre_lower = strtolower(trim($genre));
$genre_like = '%' . $genre_lower . '%';
// Check genre column, tags, and metadata JSON fields (genre and style)
// All use case-insensitive LIKE to match partial words in compound genres
$genre_condition = "AND (
LOWER(mt.genre) LIKE ?
OR LOWER(mt.tags) LIKE ?
OR LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.genre'))) LIKE ?
OR LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.style'))) LIKE ?
OR LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.genre') AS CHAR)) LIKE ?
OR LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.style') AS CHAR)) LIKE ?
)";
}
// Build comprehensive search filter - industry-leading search across ALL fields
// Searches: title, artist, genre (all locations), tags (all locations), mood, style, instruments, prompt, lyrics
// This ensures "deep" finds "Deep House", "rap" finds all rap tracks, etc.
$search_condition = '';
if (!empty($search)) {
// Use EXACT same pattern as genre filter for genre fields, then add other fields
$search_condition = "AND (
LOWER(mt.title) LIKE ?
OR LOWER(u.name) LIKE ?
OR LOWER(COALESCE(mt.prompt, '')) LIKE ?
OR LOWER(COALESCE(mt.lyrics, '')) LIKE ?
OR LOWER(mt.genre) LIKE ?
OR LOWER(mt.tags) LIKE ?
OR LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.genre'))) LIKE ?
OR LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.style'))) LIKE ?
OR LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.genre') AS CHAR)) LIKE ?
OR LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.style') AS CHAR)) LIKE ?
OR LOWER(mt.style) LIKE ?
OR LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.proGenre') AS CHAR)) LIKE ?
OR LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.proGenre'))) LIKE ?
OR LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.proSubGenre') AS CHAR)) LIKE ?
OR LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.proSubGenre'))) LIKE ?
OR LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.tags') AS CHAR)) LIKE ?
OR LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.tags'))) LIKE ?
OR LOWER(COALESCE(mt.mood, '')) LIKE ?
OR LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.mood') AS CHAR)) LIKE ?
OR LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.mood'))) LIKE ?
OR LOWER(COALESCE(mt.instruments, '')) LIKE ?
OR LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.instruments') AS CHAR)) LIKE ?
OR LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.instruments'))) LIKE ?
OR LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.proLeadInstrument') AS CHAR)) LIKE ?
OR LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.proLeadInstrument'))) LIKE ?
OR LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.proRhythmSection') AS CHAR)) LIKE ?
OR LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.proRhythmSection'))) LIKE ?
OR LOWER(COALESCE(mt.energy, '')) LIKE ?
OR LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.energy') AS CHAR)) LIKE ?
OR LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.energy'))) LIKE ?
)";
}
// Get total count for pagination FIRST (before main query) to validate page number
$count_sql = "
SELECT COUNT(*) as total
FROM music_tracks mt
INNER JOIN users u ON mt.user_id = u.id
WHERE mt.status = 'complete'
AND (mt.audio_url IS NOT NULL AND mt.audio_url != '' OR mt.variations_count > 0)
AND mt.user_id IS NOT NULL
AND u.id IS NOT NULL
AND (mt.is_public = 1 OR mt.is_public IS NULL)
$time_condition
$genre_condition
$search_condition
";
$count_stmt = $pdo->prepare($count_sql);
$count_params = [];
if ($genre !== 'all' && !empty($genre)) {
// Normalize genre parameter to lowercase for consistent case-insensitive matching
$genre_lower = strtolower(trim($genre));
$genre_like = '%' . $genre_lower . '%';
// All conditions use case-insensitive LIKE for partial word matching
$count_params = array_merge($count_params, [
$genre_like, // LOWER(mt.genre) LIKE ?
$genre_like, // LOWER(mt.tags) LIKE ?
$genre_like, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(metadata, '$.genre'))) LIKE ?
$genre_like, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(metadata, '$.style'))) LIKE ?
$genre_like, // LOWER(CAST(JSON_EXTRACT(metadata, '$.genre') AS CHAR)) LIKE ?
$genre_like // LOWER(CAST(JSON_EXTRACT(metadata, '$.style') AS CHAR)) LIKE ?
]);
}
if (!empty($search)) {
// Use lowercase search parameter for case-insensitive matching
$search_lower = strtolower(trim($search));
$search_param = '%' . $search_lower . '%';
// Add search parameters for count query - match SQL placeholders exactly (30 total)
$count_params = array_merge($count_params, [
$search_param, // LOWER(mt.title) LIKE ?
$search_param, // LOWER(u.name) LIKE ?
$search_param, // LOWER(COALESCE(mt.prompt, '')) LIKE ?
$search_param, // LOWER(COALESCE(mt.lyrics, '')) LIKE ?
$search_param, // LOWER(mt.genre) LIKE ?
$search_param, // LOWER(mt.tags) LIKE ?
$search_param, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.genre'))) LIKE ?
$search_param, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.style'))) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.genre') AS CHAR)) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.style') AS CHAR)) LIKE ?
$search_param, // LOWER(mt.style) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.proGenre') AS CHAR)) LIKE ?
$search_param, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.proGenre'))) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.proSubGenre') AS CHAR)) LIKE ?
$search_param, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.proSubGenre'))) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.tags') AS CHAR)) LIKE ?
$search_param, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.tags'))) LIKE ?
$search_param, // LOWER(COALESCE(mt.mood, '')) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.mood') AS CHAR)) LIKE ?
$search_param, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.mood'))) LIKE ?
$search_param, // LOWER(COALESCE(mt.instruments, '')) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.instruments') AS CHAR)) LIKE ?
$search_param, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.instruments'))) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.proLeadInstrument') AS CHAR)) LIKE ?
$search_param, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.proLeadInstrument'))) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.proRhythmSection') AS CHAR)) LIKE ?
$search_param, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.proRhythmSection'))) LIKE ?
$search_param, // LOWER(COALESCE(mt.energy, '')) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.energy') AS CHAR)) LIKE ?
$search_param // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.energy'))) LIKE ?
]);
}
$count_stmt->execute($count_params);
$total_tracks = $count_stmt->fetchColumn();
$total_pages = $total_tracks > 0 ? ceil($total_tracks / $per_page) : 0; // 0 pages if no tracks
// Validate page number - redirect if invalid (BEFORE main query)
if ($total_tracks == 0) {
// No tracks found - redirect to page 1 and clear genre filter if it's active
if ($genre !== 'all' && !empty($genre)) {
$redirect_url = "?page=1&sort=" . urlencode($sort) . "&time=" . urlencode($time_filter) . "&genre=all";
} else {
$redirect_url = "?page=1&sort=" . urlencode($sort) . "&time=" . urlencode($time_filter) . "&genre=" . urlencode($genre);
}
if (!empty($search)) {
$redirect_url .= "&search=" . urlencode($search);
}
header("Location: " . $redirect_url);
exit;
} elseif ($page > $total_pages) {
// Page exceeds total pages - redirect to last valid page
$redirect_url = "?page=" . $total_pages . "&sort=" . urlencode($sort) . "&time=" . urlencode($time_filter) . "&genre=" . urlencode($genre);
if (!empty($search)) {
$redirect_url .= "&search=" . urlencode($search);
}
header("Location: " . $redirect_url);
exit;
}
// Efficient query - get tracks with all needed data
// IMPORTANT: Only show tracks that actually exist and are complete
$sql = "
SELECT
mt.id,
mt.title,
mt.audio_url,
mt.duration,
mt.created_at,
mt.user_id,
mt.genre,
mt.tags,
mt.image_url,
mt.task_id,
mt.metadata,
mt.variations_count,
mt.price,
u.name as artist_name,
u.id as artist_id,
u.profile_image,
COALESCE((SELECT COUNT(*) FROM track_likes WHERE track_id = mt.id), 0) as like_count,
COALESCE((SELECT COUNT(*) FROM track_comments WHERE track_id = mt.id), 0) as comment_count,
COALESCE((SELECT COUNT(*) FROM track_plays WHERE track_id = mt.id), 0) as play_count,
COALESCE((SELECT COUNT(*) FROM track_shares WHERE track_id = mt.id), 0) as share_count,
COALESCE((SELECT COUNT(*) FROM track_likes WHERE track_id = mt.id AND user_id = ?), 0) as user_liked,
COALESCE((SELECT COUNT(*) FROM user_follows WHERE follower_id = ? AND following_id = mt.user_id), 0) as is_following,
COALESCE((SELECT COUNT(*) FROM user_wishlist WHERE user_id = ? AND track_id = mt.id), 0) as is_in_wishlist
FROM music_tracks mt
INNER JOIN users u ON mt.user_id = u.id
WHERE mt.status = 'complete'
AND (mt.audio_url IS NOT NULL AND mt.audio_url != '' OR mt.variations_count > 0)
AND mt.user_id IS NOT NULL
AND u.id IS NOT NULL
AND (mt.is_public = 1 OR mt.is_public IS NULL)
$time_condition
$genre_condition
$search_condition
ORDER BY $order_by
LIMIT $per_page OFFSET $offset
";
$stmt = $pdo->prepare($sql);
$params = [$user_id ?? 0, $user_id ?? 0, $user_id ?? 0];
if ($genre !== 'all' && !empty($genre)) {
// Normalize genre parameter to lowercase for consistent case-insensitive matching
$genre_lower = strtolower(trim($genre));
$genre_like = '%' . $genre_lower . '%';
// All conditions use case-insensitive LIKE for partial word matching
$params = array_merge($params, [
$genre_like, // LOWER(mt.genre) LIKE ?
$genre_like, // LOWER(mt.tags) LIKE ?
$genre_like, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(metadata, '$.genre'))) LIKE ?
$genre_like, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(metadata, '$.style'))) LIKE ?
$genre_like, // LOWER(CAST(JSON_EXTRACT(metadata, '$.genre') AS CHAR)) LIKE ?
$genre_like // LOWER(CAST(JSON_EXTRACT(metadata, '$.style') AS CHAR)) LIKE ?
]);
}
if (!empty($search)) {
// Use lowercase search parameter for case-insensitive matching
// This ensures "deep" matches "Deep House", "rap" matches "Rap", etc.
$search_lower = strtolower(trim($search));
$search_param = '%' . $search_lower . '%';
// Add search parameters - match SQL placeholders exactly (30 total)
// Order matches the SQL query above
$params = array_merge($params, [
$search_param, // LOWER(mt.title) LIKE ?
$search_param, // LOWER(u.name) LIKE ?
$search_param, // LOWER(COALESCE(mt.prompt, '')) LIKE ?
$search_param, // LOWER(COALESCE(mt.lyrics, '')) LIKE ?
$search_param, // LOWER(mt.genre) LIKE ?
$search_param, // LOWER(mt.tags) LIKE ?
$search_param, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.genre'))) LIKE ?
$search_param, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.style'))) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.genre') AS CHAR)) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.style') AS CHAR)) LIKE ?
$search_param, // LOWER(mt.style) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.proGenre') AS CHAR)) LIKE ?
$search_param, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.proGenre'))) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.proSubGenre') AS CHAR)) LIKE ?
$search_param, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.proSubGenre'))) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.tags') AS CHAR)) LIKE ?
$search_param, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.tags'))) LIKE ?
$search_param, // LOWER(COALESCE(mt.mood, '')) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.mood') AS CHAR)) LIKE ?
$search_param, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.mood'))) LIKE ?
$search_param, // LOWER(COALESCE(mt.instruments, '')) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.instruments') AS CHAR)) LIKE ?
$search_param, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.instruments'))) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.proLeadInstrument') AS CHAR)) LIKE ?
$search_param, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.proLeadInstrument'))) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.proRhythmSection') AS CHAR)) LIKE ?
$search_param, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.proRhythmSection'))) LIKE ?
$search_param, // LOWER(COALESCE(mt.energy, '')) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.energy') AS CHAR)) LIKE ?
$search_param // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.energy'))) LIKE ?
]);
}
$stmt->execute($params);
$tracks = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Double-check: Filter out any tracks that don't actually exist (safety check)
// This ensures deleted tracks never show up, even if there's a race condition
if (!empty($tracks)) {
$track_ids = array_column($tracks, 'id');
if (!empty($track_ids)) {
$placeholders = implode(',', array_fill(0, count($track_ids), '?'));
$verify_stmt = $pdo->prepare("SELECT id FROM music_tracks WHERE id IN ($placeholders)");
$verify_stmt->execute($track_ids);
$valid_track_ids = array_column($verify_stmt->fetchAll(PDO::FETCH_ASSOC), 'id');
// Filter tracks to only include those that still exist
$tracks = array_filter($tracks, function($track) use ($valid_track_ids) {
return in_array($track['id'], $valid_track_ids);
});
// Re-index array
$tracks = array_values($tracks);
}
}
// Get variations for each track
foreach ($tracks as &$track) {
$track['variations'] = [];
if ($track['variations_count'] > 0) {
try {
$var_stmt = $pdo->prepare("
SELECT
variation_index,
audio_url,
duration,
title,
tags,
image_url
FROM audio_variations
WHERE track_id = ?
ORDER BY variation_index ASC
");
$var_stmt->execute([$track['id']]);
$track['variations'] = $var_stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (Exception $e) {
$track['variations'] = [];
}
}
}
unset($track);
// Get community stats
$stats_stmt = $pdo->prepare("
SELECT
COUNT(DISTINCT mt.id) as total_tracks,
COUNT(DISTINCT mt.user_id) as total_artists,
COALESCE(SUM(mt.duration), 0) as total_duration
FROM music_tracks mt
INNER JOIN users u ON mt.user_id = u.id
WHERE mt.status = 'complete'
AND (mt.audio_url IS NOT NULL AND mt.audio_url != '' OR mt.variations_count > 0)
AND mt.user_id IS NOT NULL
AND u.id IS NOT NULL
AND (mt.is_public = 1 OR mt.is_public IS NULL)
");
$stats_stmt->execute();
$stats = $stats_stmt->fetch(PDO::FETCH_ASSOC);
?>
<div class="community-page">
<!-- Hero Section -->
<div class="community-hero">
<div class="container">
<div class="hero-header">
<div class="hero-title-section">
<h1><?= t('community.hero_title') ?></h1>
<p class="subtitle"><?= t('community.hero_subtitle') ?></p>
</div>
<button class="share-community-btn" onclick="shareCommunityFeed()" title="Share Community Feed">
<i class="fas fa-share-alt"></i>
<span>Share Feed</span>
</button>
</div>
<!-- Quick Stats -->
<div class="quick-stats">
<div class="stat-item">
<span class="stat-number"><?= number_format($stats['total_tracks']) ?></span>
<span class="stat-label"><?= t('community.tracks') ?></span>
</div>
<div class="stat-item">
<span class="stat-number"><?= number_format($stats['total_artists']) ?></span>
<span class="stat-label"><?= t('community.artists') ?></span>
</div>
<div class="stat-item">
<span class="stat-number"><?= gmdate('H:i:s', $stats['total_duration']) ?></span>
<span class="stat-label"><?= t('community.total_music') ?></span>
</div>
</div>
</div>
</div>
<!-- Filters & Controls -->
<div class="community-filters">
<div class="container">
<div class="filters-row">
<!-- Search Input -->
<div class="filter-group search-group">
<label><?= t('community.search') ?>:</label>
<input type="text"
id="searchInput"
class="search-input"
placeholder="<?= t('community.search_placeholder') ?>"
value="<?= htmlspecialchars($search) ?>"
onkeypress="if(event.key === 'Enter') updateFilters()">
<button class="search-btn" onclick="updateFilters()" title="<?= t('community.search') ?>">
<i class="fas fa-search"></i>
</button>
</div>
<!-- Sort Options -->
<div class="filter-group">
<label><?= t('community.sort') ?>:</label>
<select id="sortSelect" onchange="updateFilters()">
<option value="latest" <?= $sort === 'latest' ? 'selected' : '' ?>><?= t('community.latest') ?></option>
<option value="trending" <?= $sort === 'trending' ? 'selected' : '' ?>><?= t('community.trending') ?></option>
<option value="popular" <?= $sort === 'popular' ? 'selected' : '' ?>><?= t('community.popular') ?></option>
<option value="random" <?= $sort === 'random' ? 'selected' : '' ?>><?= t('community.random') ?></option>
</select>
</div>
<!-- Time Filter -->
<div class="filter-group">
<label><?= t('community.time') ?>:</label>
<select id="timeSelect" onchange="updateFilters()">
<option value="all" <?= $time_filter === 'all' ? 'selected' : '' ?>><?= t('community.all_time') ?></option>
<option value="today" <?= $time_filter === 'today' ? 'selected' : '' ?>><?= t('community.today') ?></option>
<option value="week" <?= $time_filter === 'week' ? 'selected' : '' ?>><?= t('community.this_week') ?></option>
<option value="month" <?= $time_filter === 'month' ? 'selected' : '' ?>><?= t('community.this_month') ?></option>
</select>
</div>
<!-- Genre Filter -->
<div class="filter-group">
<label><?= t('community.genre') ?>:</label>
<select id="genreSelect" onchange="updateFilters()">
<option value="all" <?= $genre === 'all' ? 'selected' : '' ?>><?= t('community.all_genres') ?></option>
<option value="Electronic" <?= $genre === 'Electronic' ? 'selected' : '' ?>>Electronic</option>
<option value="Pop" <?= $genre === 'Pop' ? 'selected' : '' ?>>Pop</option>
<option value="Hip Hop" <?= $genre === 'Hip Hop' ? 'selected' : '' ?>>Hip Hop</option>
<option value="Rock" <?= $genre === 'Rock' ? 'selected' : '' ?>>Rock</option>
<option value="Jazz" <?= $genre === 'Jazz' ? 'selected' : '' ?>>Jazz</option>
<option value="Classical" <?= $genre === 'Classical' ? 'selected' : '' ?>>Classical</option>
<option value="Ambient" <?= $genre === 'Ambient' ? 'selected' : '' ?>>Ambient</option>
</select>
</div>
</div>
</div>
</div>
<!-- Tracks Grid -->
<div class="container">
<?php if (empty($tracks)): ?>
<div class="empty-state">
<i class="fas fa-music"></i>
<h2><?= t('community.no_tracks_found') ?></h2>
<p><?= t('community.no_tracks_desc') ?></p>
</div>
<?php else: ?>
<div class="tracks-grid" id="tracksGrid" data-current-page="1" data-has-more="<?= $page < $total_pages ? 'true' : 'false' ?>">
<?php foreach ($tracks as $track):
$displayTitle = !empty($track['title']) ? htmlspecialchars($track['title']) : t('community.untitled_track');
$duration = $track['duration'] ? gmdate('i:s', $track['duration']) : '0:00';
$isWishlisted = !empty($track['is_in_wishlist']);
// Handle track image - use original image_url from database (should always be local path)
$imageUrl = $track['image_url'] ?? null;
// Trim whitespace and check for empty/null values
if ($imageUrl !== null) {
$imageUrl = trim($imageUrl);
if ($imageUrl === '' || $imageUrl === 'null' || $imageUrl === 'NULL') {
$imageUrl = null;
}
}
// If image_url exists and is not empty/null, normalize and use it
if (!empty($imageUrl)) {
// Normalize local paths (add leading / if missing)
if (!str_starts_with($imageUrl, '/')) {
$imageUrl = '/' . ltrim($imageUrl, '/');
}
} else {
// Only if image_url is empty/null, try fallback sources
if (!empty($track['metadata'])) {
$metadata = is_string($track['metadata']) ? json_decode($track['metadata'], true) : $track['metadata'];
if (isset($metadata['image_url']) && !empty($metadata['image_url'])) {
$metaImageUrl = $metadata['image_url'];
// Only use if it's a local path (metadata might have external URLs)
if (strpos($metaImageUrl, 'http://') !== 0 && strpos($metaImageUrl, 'https://') !== 0) {
if (!str_starts_with($metaImageUrl, '/')) {
$metaImageUrl = '/' . ltrim($metaImageUrl, '/');
}
$imageUrl = $metaImageUrl;
}
} elseif (isset($metadata['cover_url']) && !empty($metadata['cover_url'])) {
$metaCoverUrl = $metadata['cover_url'];
// Only use if it's a local path (metadata might have external URLs)
if (strpos($metaCoverUrl, 'http://') !== 0 && strpos($metaCoverUrl, 'https://') !== 0) {
if (!str_starts_with($metaCoverUrl, '/')) {
$metaCoverUrl = '/' . ltrim($metaCoverUrl, '/');
}
$imageUrl = $metaCoverUrl;
}
}
}
// Try to find image file by task_id pattern
if (empty($imageUrl) && !empty($track['task_id'])) {
$uploadsDir = $_SERVER['DOCUMENT_ROOT'] . '/uploads/track_covers/';
if (is_dir($uploadsDir)) {
$pattern = $uploadsDir . "track_{$track['task_id']}_*";
$files = glob($pattern);
if (!empty($files)) {
$mostRecent = end($files);
$imageUrl = '/uploads/track_covers/' . basename($mostRecent);
}
}
}
// Fallback to default only if no image found
if (empty($imageUrl)) {
$imageUrl = '/assets/images/default-track.jpg';
}
}
// CRITICAL: Check for selected variation and use its audio URL if available
$selectedVariationIndex = null;
$selectedVariation = null;
// Parse metadata to get selected_variation
if (!empty($track['metadata'])) {
$trackMetadata = is_string($track['metadata']) ? json_decode($track['metadata'], true) : $track['metadata'];
if (isset($trackMetadata['selected_variation'])) {
$selectedVariationIndex = (int)$trackMetadata['selected_variation'];
}
}
// Also check the selected_variation column if it exists
if ($selectedVariationIndex === null && isset($track['selected_variation'])) {
$selectedVariationIndex = (int)$track['selected_variation'];
}
// Normalize variation audio URLs first
if (!empty($track['variations'])) {
foreach ($track['variations'] as &$var) {
$varAudioUrl = $var['audio_url'] ?? '';
if ($varAudioUrl && !preg_match('/^https?:\/\//', $varAudioUrl) && !str_starts_with($varAudioUrl, '/')) {
// Fix malformed URLs like "audio_files=filename.mp3" to "/audio_files/filename.mp3"
if (strpos($varAudioUrl, 'audio_files=') === 0) {
$var['audio_url'] = '/' . str_replace('audio_files=', 'audio_files/', $varAudioUrl);
} else {
$var['audio_url'] = '/' . ltrim($varAudioUrl, '/');
}
} else {
$var['audio_url'] = $varAudioUrl;
}
// Find the selected variation
if ($selectedVariationIndex !== null && isset($var['variation_index']) && $var['variation_index'] == $selectedVariationIndex) {
$selectedVariation = $var;
}
}
unset($var); // Break reference
// Fallback: if selected variation not found by index, try array position
if (!$selectedVariation && $selectedVariationIndex !== null && isset($track['variations'][$selectedVariationIndex])) {
$selectedVariation = $track['variations'][$selectedVariationIndex];
}
}
// Use selected variation's audio URL if available, otherwise use main track's audio URL
$mainAudioUrl = '';
if ($selectedVariation && !empty($selectedVariation['audio_url'])) {
$mainAudioUrl = $selectedVariation['audio_url'];
// Also update duration if available
if (!empty($selectedVariation['duration'])) {
$duration = gmdate('i:s', $selectedVariation['duration']);
}
} else {
$mainAudioUrl = $track['audio_url'] ?? '';
// If main track URL is empty/invalid and we have variations, use first variation as fallback
if (empty($mainAudioUrl) && !empty($track['variations']) && is_array($track['variations'])) {
$firstVariation = reset($track['variations']);
if (!empty($firstVariation['audio_url'])) {
$mainAudioUrl = $firstVariation['audio_url'];
if (!empty($firstVariation['duration'])) {
$duration = gmdate('i:s', $firstVariation['duration']);
}
}
}
}
// Use signed proxy URL to hide actual MP3 file paths and prevent URL sharing
// Include variation index if a selected variation exists
if ($selectedVariationIndex !== null) {
$mainAudioUrl = getSignedAudioUrl($track['id'], $selectedVariationIndex);
} else {
$mainAudioUrl = getSignedAudioUrl($track['id']);
}
?>
<div class="track-card"
data-track-id="<?= $track['id'] ?>"
itemscope
itemtype="http://schema.org/MusicRecording">
<!-- Track Image -->
<div class="track-image-wrapper">
<img src="<?= htmlspecialchars($imageUrl) ?>" alt="<?= $displayTitle ?>" class="track-image" loading="lazy">
<div class="track-overlay">
<button class="play-btn"
data-track-id="<?= $track['id'] ?>"
data-audio-url="<?= htmlspecialchars($mainAudioUrl ?? '', ENT_QUOTES) ?>"
data-title="<?= htmlspecialchars($displayTitle ?? '', ENT_QUOTES) ?>"
data-artist="<?= htmlspecialchars($track['artist_name'] ?? '', ENT_QUOTES) ?>">
<i class="fas fa-play"></i>
</button>
<div class="track-duration"><?= $duration ?></div>
</div>
</div>
<!-- Track Info -->
<div class="track-info">
<div class="track-title-row">
<a href="/track.php?id=<?= $track['id'] ?>"
class="track-title"
title="<?= $displayTitle ?>"
itemprop="name"><?= $displayTitle ?></a>
<?php if (isset($track['price']) && $track['price'] > 0): ?>
<span class="for-sale-badge" title="<?= t('community.available_for_purchase') ?>">
<i class="fas fa-tag"></i>
<span><?= t('community.for_sale') ?></span>
</span>
<?php endif; ?>
</div>
<a href="/artist_profile.php?id=<?= $track['artist_id'] ?>"
class="track-artist"
itemprop="byArtist"
itemscope
itemtype="http://schema.org/MusicGroup">
<span itemprop="name"><?= htmlspecialchars($track['artist_name']) ?></span>
</a>
<?php
// Extract genre and mood like artist_profile.php
$genre = 'Electronic'; // default
$mood = null;
// Try to extract genre/mood from metadata if available
if (!empty($track['metadata'])) {
$metadata = is_string($track['metadata']) ? json_decode($track['metadata'], true) : $track['metadata'];
if ($metadata) {
// Check metadata.genre first, then metadata.style (some tracks store genre in style)
if (!empty($metadata['genre'])) {
$genre = trim($metadata['genre']);
} elseif (!empty($metadata['style'])) {
$genre = trim($metadata['style']);
}
if (!empty($metadata['mood'])) {
$mood = trim($metadata['mood']);
}
}
}
// Use track genre if metadata doesn't have it
if (($genre === 'Electronic' || empty($genre)) && !empty($track['genre'])) {
$genre = trim($track['genre']);
}
// Ensure genre is not empty
if (empty($genre)) {
$genre = 'Electronic';
}
?>
<div class="track-genre">
<a href="?genre=<?= urlencode($genre) ?>" class="genre-tag-link"><?= htmlspecialchars($genre) ?></a>
<?php if ($mood && $mood !== $genre && strtolower($mood) !== 'neutral'): ?>
<a href="?genre=<?= urlencode($mood) ?>" class="genre-tag-link"><?= htmlspecialchars($mood) ?></a>
<?php endif; ?>
</div>
<?php if (!empty($track['created_at'])): ?>
<div class="track-date">
<i class="fas fa-calendar-alt"></i>
<span><?= date('M j, Y', strtotime($track['created_at'])) ?></span>
</div>
<?php endif; ?>
</div>
<!-- Track Stats -->
<div class="track-stats">
<button class="stat-btn" title="Plays">
<i class="fas fa-headphones-alt"></i>
<span><?= number_format($track['play_count']) ?></span>
</button>
<button class="stat-btn like-btn <?= $track['user_liked'] ? 'liked' : '' ?>" onclick="toggleLike(<?= $track['id'] ?>, this)">
<i class="fas fa-heart"></i>
<span><?= number_format($track['like_count']) ?></span>
</button>
<button class="stat-btn" onclick="showComments(<?= $track['id'] ?>)">
<i class="fas fa-comment"></i>
<span><?= number_format($track['comment_count']) ?></span>
</button>
<button class="stat-btn" onclick="shareTrack(<?= $track['id'] ?>)">
<i class="fas fa-share"></i>
<span><?= number_format($track['share_count'] ?? 0) ?></span>
</button>
<?php if (!empty($track['variations'])): ?>
<button class="stat-btn variations-btn" onclick="showVariations(<?= $track['id'] ?>, this)" title="<?= count($track['variations']) ?> <?= t('community.variations') ?>">
<i class="fas fa-layer-group"></i>
<span><?= count($track['variations']) ?></span>
</button>
<?php endif; ?>
<?php if ($user_id && $user_id != $track['user_id']): ?>
<button class="stat-btn follow-btn <?= $track['is_following'] ? 'following' : '' ?>" onclick="toggleFollow(<?= $track['user_id'] ?>, this)">
<i class="fas fa-user-plus"></i>
</button>
<?php endif; ?>
</div>
<!-- Add to Cart Button -->
<div class="track-cart-section">
<button class="add-to-cart-btn"
type="button"
data-track-id="<?= $track['id'] ?>"
data-track-title="<?= htmlspecialchars($displayTitle, ENT_QUOTES | ENT_HTML5, 'UTF-8') ?>"
data-track-price="<?= isset($track['price']) && $track['price'] !== null ? (float)$track['price'] : 1.99 ?>"
onclick="addToCart(<?= $track['id'] ?>, '<?= htmlspecialchars($displayTitle) ?>', <?= isset($track['price']) && $track['price'] !== null ? (float)$track['price'] : 1.99 ?>, this)">
<i class="fas fa-shopping-cart"></i>
<span><?= t('community.add_to_cart') ?></span>
<?php
// Handle price display: null/not set = 1.99, 0 = free, > 0 = actual price
$displayPrice = 1.99; // default
if (isset($track['price']) && $track['price'] !== null && $track['price'] !== '') {
$displayPrice = (float)$track['price'];
}
?>
<span class="cart-price"><?= $displayPrice > 0 ? '$' . number_format($displayPrice, 2) : t('common.free') ?></span>
</button>
<button class="wishlist-inline-btn <?= $isWishlisted ? 'active' : '' ?>"
onclick="toggleWishlist(<?= $track['id'] ?>, this)"
aria-pressed="<?= $isWishlisted ? 'true' : 'false' ?>">
<i class="<?= $isWishlisted ? 'fas' : 'far' ?> fa-heart"></i>
</button>
</div>
<!-- Variations Container (hidden by default) -->
<?php if (!empty($track['variations'])): ?>
<div class="variations-container" id="variations-<?= $track['id'] ?>" style="display: none;">
<div class="variations-header">
<span><?= t('community.audio_variations') ?> (<?= count($track['variations']) ?>)</span>
</div>
<div class="variations-grid">
<?php foreach ($track['variations'] as $var):
// Use main track title with variation number for consistency
$variationDisplayTitle = $displayTitle . ' - ' . t('community.variation') . ' ' . ($var['variation_index'] + 1);
?>
<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(getSignedAudioUrl($track['id'], $var['variation_index'])) ?>"
data-title="<?= htmlspecialchars($variationDisplayTitle) ?>"
data-artist="<?= htmlspecialchars($track['artist_name'] ?? '', ENT_QUOTES) ?>">
<i class="fas fa-play"></i>
</button>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<!-- Infinite Scroll Loading Indicator -->
<div id="infiniteScrollLoader" style="display: none; text-align: center; padding: 40px 20px; color: white;">
<div class="loading-spinner" style="font-size: 2rem; margin-bottom: 1rem;">âŗ</div>
<p style="font-size: 1.1rem; font-weight: 600;"><?= t('community.loading_more') ?></p>
</div>
<!-- End of Results Message -->
<div id="endOfResults" style="display: none; text-align: center; padding: 40px 20px; color: white;">
<p style="font-size: 1.1rem; font-weight: 600; opacity: 0.8;"><?= t('community.no_more_tracks') ?></p>
</div>
<?php endif; ?>
</div>
</div>
<style>
/* Animated gradient background */
@keyframes gradientShift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.community-page {
min-height: 100vh;
background: linear-gradient(-45deg, #667eea, #764ba2, #f093fb, #4facfe);
background-size: 400% 400%;
animation: gradientShift 15s ease infinite;
padding-bottom: 100px;
position: relative;
overflow-x: hidden;
}
/* Floating particles effect */
.community-page::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image:
radial-gradient(circle at 20% 50%, rgba(255,255,255,0.1) 0%, transparent 50%),
radial-gradient(circle at 80% 80%, rgba(255,255,255,0.1) 0%, transparent 50%),
radial-gradient(circle at 40% 20%, rgba(255,255,255,0.1) 0%, transparent 50%);
pointer-events: none;
z-index: 0;
}
.community-hero {
position: relative;
z-index: 1;
background: rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px);
padding: 80px 20px;
text-align: center;
color: white;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.hero-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 20px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.hero-title-section {
flex: 1;
min-width: 300px;
}
.community-hero h1 {
font-size: 3.5rem;
margin: 0 0 15px 0;
font-weight: 700;
text-shadow: 0 4px 20px rgba(0,0,0,0.3);
animation: fadeInUp 0.8s ease;
}
.community-hero .subtitle {
font-size: 1.2rem;
opacity: 0.95;
margin-bottom: 40px;
font-weight: 400;
animation: fadeInUp 0.8s ease 0.2s both;
}
.share-community-btn {
display: flex;
align-items: center;
gap: 10px;
padding: 16px 32px;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.9), rgba(118, 75, 162, 0.9));
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 30px;
color: white;
font-size: 1.2rem;
font-weight: 700;
cursor: pointer;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
white-space: nowrap;
animation: fadeInUp 0.8s ease 0.3s both;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
min-height: 50px;
}
.share-community-btn:hover {
background: linear-gradient(135deg, rgba(102, 126, 234, 1), rgba(118, 75, 162, 1));
border-color: rgba(255, 255, 255, 0.5);
transform: translateY(-3px);
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4);
}
.share-community-btn:active {
transform: translateY(-1px);
}
.share-community-btn i {
font-size: 1.3rem;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.quick-stats {
display: flex;
justify-content: center;
gap: 50px;
margin-top: 40px;
flex-wrap: wrap;
animation: fadeInUp 0.8s ease 0.4s both;
}
.stat-item {
text-align: center;
padding: 20px 30px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 15px;
border: 1px solid rgba(255, 255, 255, 0.2);
transition: all 0.3s ease;
cursor: default;
}
.stat-item:hover {
transform: translateY(-5px) scale(1.05);
background: rgba(255, 255, 255, 0.15);
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
}
.stat-number {
display: block;
font-size: 2.5rem;
font-weight: 800;
margin-bottom: 8px;
color: white;
}
.stat-label {
font-size: 0.85rem;
opacity: 0.9;
text-transform: uppercase;
letter-spacing: 2px;
font-weight: 600;
}
.community-filters {
position: relative;
z-index: 1;
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(20px);
padding: 25px;
margin-bottom: 40px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.filters-row {
display: flex;
gap: 25px;
justify-content: center;
flex-wrap: wrap;
align-items: center;
}
.filter-group {
display: flex;
align-items: center;
gap: 12px;
}
.filter-group label {
color: white;
font-weight: 600;
font-size: 0.95rem;
text-shadow: 0 2px 10px rgba(0,0,0,0.2);
}
.filter-group select {
padding: 10px 18px;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 12px;
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10px);
color: white;
font-size: 0.95rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
outline: none;
}
.search-group {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
max-width: 400px;
}
.search-input {
flex: 1;
padding: 10px 18px;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 12px;
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10px);
color: white;
font-size: 0.95rem;
font-weight: 500;
transition: all 0.3s ease;
outline: none;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.search-input::placeholder {
color: rgba(255, 255, 255, 0.5);
}
.search-input:focus {
background: rgba(255, 255, 255, 0.25);
border-color: rgba(255, 255, 255, 0.6);
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
.search-btn {
padding: 10px 18px;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.95rem;
}
.search-btn:hover {
background: linear-gradient(135deg, #5a67d8 0%, #6b46c1 100%);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
}
.filter-group select:hover {
background: rgba(255, 255, 255, 0.25);
border-color: rgba(255, 255, 255, 0.5);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
.filter-group select:focus {
background: rgba(255, 255, 255, 0.25);
border-color: rgba(255, 255, 255, 0.6);
}
.filter-group select option {
background: #667eea;
color: white;
padding: 10px;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 0 20px;
position: relative;
z-index: 1;
}
.tracks-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 30px;
margin-top: 40px;
}
.track-card {
background: rgba(255, 255, 255, 0.98);
border-radius: 20px;
overflow: hidden;
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
cursor: pointer;
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
border: 1px solid rgba(255, 255, 255, 0.3);
animation: cardFadeIn 0.6s ease both;
}
@keyframes cardFadeIn {
from {
opacity: 0;
transform: translateY(20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.track-card:nth-child(1) { animation-delay: 0.1s; }
.track-card:nth-child(2) { animation-delay: 0.15s; }
.track-card:nth-child(3) { animation-delay: 0.2s; }
.track-card:nth-child(4) { animation-delay: 0.25s; }
.track-card:nth-child(5) { animation-delay: 0.3s; }
.track-card:nth-child(6) { animation-delay: 0.35s; }
.track-card:hover {
transform: translateY(-10px) scale(1.02);
box-shadow: 0 20px 40px rgba(0,0,0,0.3);
border-color: rgba(255, 255, 255, 0.5);
}
.track-image-wrapper {
position: relative;
width: 100%;
padding-top: 100%;
overflow: hidden;
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
}
.track-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s ease;
}
.track-card:hover .track-image {
transform: scale(1.1) rotate(2deg);
}
.track-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.8) 0%, rgba(118, 75, 162, 0.8) 100%);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: all 0.4s ease;
}
.track-card:hover .track-overlay {
opacity: 1;
}
.play-btn {
width: 70px;
height: 70px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.95);
border: 3px solid rgba(255, 255, 255, 0.5);
color: #667eea;
font-size: 1.8rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
transform: scale(0.9);
}
.track-card:hover .play-btn {
transform: scale(1);
}
.play-btn:hover {
transform: scale(1.15);
box-shadow: 0 15px 40px rgba(0,0,0,0.4);
background: white;
}
.play-btn:active {
transform: scale(0.95);
}
.track-duration {
position: absolute;
bottom: 12px;
right: 12px;
background: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(10px);
color: white;
padding: 6px 12px;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.5px;
}
.track-info {
padding: 18px 20px 20px;
background: white;
}
.track-title-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
margin-bottom: 10px;
min-height: 48px;
}
.track-title {
font-size: 1.3rem;
font-weight: 700;
margin: 0;
color: #0f172a;
line-height: 1.4;
text-decoration: none;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
cursor: pointer;
transition: all 0.3s ease;
flex: 1;
min-width: 0;
letter-spacing: -0.02em;
word-break: break-word;
}
.for-sale-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
background: rgba(46, 204, 113, 0.1);
color: #2ecc71;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
white-space: nowrap;
border: 1px solid rgba(46, 204, 113, 0.2);
transition: all 0.2s ease;
}
.for-sale-badge:hover {
background: rgba(46, 204, 113, 0.15);
border-color: rgba(46, 204, 113, 0.3);
}
.for-sale-badge i {
font-size: 0.7rem;
}
.track-title:hover {
color: #667eea;
text-decoration: none;
transform: translateX(2px);
}
.track-artist {
color: #475569;
text-decoration: none;
font-size: 1rem;
font-weight: 600;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 6px;
margin-top: 2px;
letter-spacing: 0.01em;
}
.track-artist::before {
content: 'by';
font-size: 0.85rem;
font-weight: 500;
color: #94a3b8;
opacity: 0.7;
}
.track-artist span {
color: inherit;
}
.track-artist:hover {
color: #667eea;
transform: translateX(3px);
}
.track-artist:hover::before {
color: #a5b4fc;
opacity: 1;
}
.track-genre {
margin-top: 6px;
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-bottom: 6px;
}
.genre-tag-link {
background: rgba(102, 126, 234, 0.15);
color: #a5b4fc;
padding: 6px 12px;
text-decoration: none;
border-radius: 12px;
font-size: 0.85rem;
font-weight: 600;
border: 1px solid rgba(102, 126, 234, 0.2);
display: inline-block;
transition: all 0.2s ease;
text-transform: capitalize;
letter-spacing: 0.3px;
}
.genre-tag-link:hover {
background: rgba(102, 126, 234, 0.25);
color: #8b9aff;
border-color: rgba(102, 126, 234, 0.4);
transform: translateY(-1px);
}
.track-date {
display: flex;
align-items: center;
gap: 6px;
color: #999;
font-size: 0.8rem;
margin-top: 6px;
font-weight: 400;
}
.track-date i {
font-size: 0.75rem;
opacity: 0.7;
}
.track-stats {
display: flex;
padding: 12px 20px;
border-top: 1px solid #f0f0f0;
gap: 8px;
background: #fafafa;
}
.stat-btn {
flex: 1;
padding: 10px;
border: none;
background: transparent;
color: #666;
cursor: pointer;
border-radius: 10px;
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
font-size: 0.85rem;
font-weight: 600;
}
.stat-btn:hover {
background: linear-gradient(135deg, #667eea15, #764ba215);
color: #667eea;
transform: translateY(-2px);
}
.stat-btn.liked {
color: #e74c3c;
background: rgba(231, 76, 60, 0.1);
}
.stat-btn.following {
color: #667eea;
background: rgba(102, 126, 234, 0.1);
}
.stat-btn.variations-btn {
color: #764ba2;
}
.stat-btn.variations-btn:hover {
background: rgba(118, 75, 162, 0.1);
color: #764ba2;
}
.track-cart-section {
padding: 12px 20px;
border-top: 1px solid #f0f0f0;
background: white;
display: flex;
align-items: center;
gap: 12px;
}
.add-to-cart-btn {
width: 100%;
padding: 12px 20px;
border: none;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 10px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.2);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.add-to-cart-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
background: linear-gradient(135deg, #5a67d8 0%, #6b46c1 100%);
}
.add-to-cart-btn:active {
transform: translateY(0);
}
.add-to-cart-btn i {
font-size: 1rem;
}
.add-to-cart-btn .cart-price {
margin-left: auto;
font-weight: 700;
opacity: 0.95;
}
.track-cart-section .add-to-cart-btn {
flex: 1;
}
.add-to-cart-btn.added {
background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
box-shadow: 0 4px 15px rgba(72, 187, 120, 0.3);
}
.add-to-cart-btn.added:hover {
background: linear-gradient(135deg, #38a169 0%, #2f855a 100%);
box-shadow: 0 6px 20px rgba(72, 187, 120, 0.4);
}
.wishlist-inline-btn {
margin-left: 12px;
width: 48px;
height: 48px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.25);
background: rgba(255, 255, 255, 0.08);
color: #ff7aa2;
font-size: 1.2rem;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.wishlist-inline-btn:hover {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.4);
}
.wishlist-inline-btn.active {
background: rgba(255, 99, 132, 0.2);
border-color: rgba(255, 99, 132, 0.4);
color: #ff6384;
}
.variations-container {
padding: 15px 20px;
background: #f8f9fa;
border-top: 1px solid #e9ecef;
}
.variations-header {
font-weight: 600;
color: #333;
margin-bottom: 12px;
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.variations-grid {
display: flex;
flex-direction: column;
gap: 8px;
}
.variation-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
background: white;
border-radius: 8px;
border: 1px solid #e9ecef;
transition: all 0.2s;
}
.variation-item:hover {
background: #f8f9fa;
border-color: #667eea;
}
.variation-info {
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
}
.variation-title {
font-size: 0.9rem;
font-weight: 500;
color: #333;
}
.variation-duration {
font-size: 0.75rem;
color: #666;
}
.variation-play-btn {
width: 36px;
height: 36px;
border-radius: 50%;
border: none;
background: #667eea;
color: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
font-size: 0.9rem;
}
.variation-play-btn:hover {
background: #764ba2;
transform: scale(1.1);
}
.empty-state {
text-align: center;
padding: 100px 20px;
color: white;
}
.empty-state i {
font-size: 5rem;
opacity: 0.6;
margin-bottom: 25px;
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-20px); }
}
.empty-state h2 {
margin: 0 0 15px 0;
font-size: 2rem;
font-weight: 700;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 25px;
margin-top: 60px;
padding: 30px 20px;
}
.pagination-btn {
padding: 12px 25px;
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10px);
color: white;
text-decoration: none;
border-radius: 12px;
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.pagination-btn:hover {
background: rgba(255, 255, 255, 0.25);
transform: translateY(-3px);
box-shadow: 0 10px 25px rgba(0,0,0,0.2);
}
.pagination-info {
color: white;
font-weight: 700;
font-size: 1.1rem;
text-shadow: 0 2px 10px rgba(0,0,0,0.2);
}
@media (max-width: 768px) {
.hero-header {
flex-direction: column;
align-items: center;
text-align: center;
}
.hero-title-section {
width: 100%;
min-width: auto;
}
.share-community-btn {
width: 100%;
max-width: 300px;
justify-content: center;
padding: 18px 32px;
font-size: 1.1rem;
}
.tracks-grid {
grid-template-columns: 1fr !important;
gap: 20px;
}
.track-card {
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
.community-hero h1 {
font-size: 2.2rem;
}
.quick-stats {
gap: 15px;
}
.stat-item {
padding: 15px 20px;
}
.stat-number {
font-size: 1.8rem;
}
.filters-row {
gap: 15px;
}
.genre-tag-link {
font-size: 0.8rem;
padding: 5px 10px;
}
}
@media (max-width: 480px) {
.hero-header {
padding: 1rem;
}
.community-hero h1 {
font-size: 1.8rem;
}
.tracks-grid {
grid-template-columns: 1fr !important;
gap: 15px;
}
.track-card {
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
.track-info {
padding: 14px 16px 16px;
}
.track-title-row {
min-height: 40px;
margin-bottom: 6px;
gap: 8px;
}
.track-title {
font-size: 1.1rem;
line-height: 1.3;
-webkit-line-clamp: 2;
}
.track-artist {
font-size: 0.9rem;
margin-top: 0;
}
.track-artist::before {
font-size: 0.75rem;
}
.quick-stats {
gap: 10px;
}
.stat-item {
padding: 12px 15px;
}
.stat-number {
font-size: 1.5rem;
}
.filters-row {
flex-direction: column;
gap: 10px;
}
.track-genre {
margin-top: 4px;
}
.genre-tag-link {
font-size: 0.75rem;
padding: 5px 10px;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: inline-block;
}
.track-info {
padding: 16px 18px 18px;
}
.track-title-row {
min-height: 44px;
margin-bottom: 8px;
}
.track-title {
font-size: 1.2rem;
line-height: 1.35;
-webkit-line-clamp: 2;
}
.track-artist {
font-size: 0.95rem;
}
.track-artist::before {
font-size: 0.8rem;
}
}
</style>
<script>
function updateFilters() {
const sort = document.getElementById('sortSelect').value;
const time = document.getElementById('timeSelect').value;
const genre = document.getElementById('genreSelect').value;
const search = document.getElementById('searchInput').value.trim();
let url = `?sort=${sort}&time=${time}&genre=${genre}&page=1`;
if (search) {
url += `&search=${encodeURIComponent(search)}`;
}
window.location.href = url;
}
// NOTE: playTrack function is now defined AFTER footer.php loads global_player.php (see bottom of file)
// This ensures the global player is loaded before we try to use it
// Record track play count
function recordTrackPlay(trackId) {
// Only record if not already recorded recently
const lastPlayed = sessionStorage.getItem(`played_${trackId}`);
const now = Date.now();
if (!lastPlayed || (now - parseInt(lastPlayed)) > 30000) { // 30 seconds minimum between plays
fetch('/api_social.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'play', track_id: trackId })
})
.then(response => response.json())
.then(data => {
if (data.success) {
sessionStorage.setItem(`played_${trackId}`, now.toString());
console.log('đĩ Play count recorded for track:', trackId);
}
})
.catch(error => {
console.warn('đĩ Play count error:', error);
});
}
}
function toggleLike(trackId, button) {
fetch('/api/toggle_like.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
credentials: 'include',
body: JSON.stringify({track_id: trackId})
})
.then(response => response.json())
.then(data => {
if (!data.success) {
if (typeof showNotification === 'function') {
showNotification(data.error || 'Unable to like this track right now.', 'error');
} else {
alert(data.error || 'Unable to like this track right now.');
}
return;
}
button.classList.toggle('liked', !!data.liked);
const countSpan = button.querySelector('span');
if (countSpan) {
countSpan.textContent = (data.like_count || 0).toLocaleString();
}
})
.catch(error => {
console.error('toggleLike error', error);
if (typeof showNotification === 'function') {
showNotification('Network error while liking track. Please try again.', 'error');
} else {
alert('Network error while liking track. Please try again.');
}
});
}
function toggleFollow(userId, button) {
fetch('/api/toggle_follow.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({user_id: userId})
})
.then(r => r.json())
.then(data => {
if (data.success) {
button.classList.toggle('following');
}
});
}
function showComments(trackId) {
window.location.href = `/track.php?id=${trackId}#comments`;
}
function shareTrack(trackId) {
// Find the track card to get title and artist
const trackCard = document.querySelector(`.track-card[data-track-id="${trackId}"]`);
const trackTitleElement = trackCard?.querySelector('.track-title');
const trackArtistElement = trackCard?.querySelector('.track-artist');
const trackTitle = trackTitleElement?.textContent?.trim() || 'Track';
const artistName = trackArtistElement?.textContent?.trim() || 'Artist';
const url = `${window.location.origin}/track.php?id=${trackId}`;
const shareText = `Check out "${trackTitle}" by ${artistName} on SoundStudioPro! đĩ`;
const shareData = {
title: `${trackTitle} by ${artistName}`,
text: shareText,
url: url
};
// Record the share in the database (don't wait for it)
const platform = 'web';
fetch('/api_social.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'share',
track_id: trackId,
platform: platform
})
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
if (data.success) {
console.log('đ Share recorded for track:', trackId, 'New count:', data.share_count);
// Update share count in UI - use the count from server
const shareButtons = document.querySelectorAll(`button[onclick*="shareTrack(${trackId})"]`);
shareButtons.forEach(button => {
const countSpan = button.querySelector('span');
if (countSpan) {
// Use the count from server if available, otherwise increment
if (data.share_count !== undefined) {
countSpan.textContent = parseInt(data.share_count).toLocaleString();
} else {
const currentCount = parseInt(countSpan.textContent.replace(/,/g, '')) || 0;
countSpan.textContent = (currentCount + 1).toLocaleString();
}
}
});
} else {
console.warn('đ Share recording failed:', data.message || 'Unknown error');
}
})
.catch(error => {
console.error('đ Share recording error:', error);
});
// Try native share API first (mobile devices, especially Android)
if (navigator.share) {
// Check if we can share this data (Android requirement)
if (navigator.canShare && navigator.canShare(shareData)) {
navigator.share(shareData)
.then(() => {
console.log('đ Track shared successfully via native API');
if (typeof showNotification === 'function') {
showNotification('â
Track shared successfully!', 'success');
}
})
.catch((error) => {
// User cancelled or error - fallback to clipboard
if (error.name !== 'AbortError') {
console.log('đ Share API failed, using clipboard:', error);
copyTrackUrlToClipboard(url, shareText);
}
});
} else if (!navigator.canShare) {
// canShare not available, try share anyway
navigator.share(shareData)
.then(() => {
console.log('đ Track shared successfully via Web Share API');
if (typeof showNotification === 'function') {
showNotification('â
Track shared successfully!', 'success');
}
})
.catch((error) => {
// User cancelled or error - fallback to clipboard
if (error.name !== 'AbortError') {
console.log('đ Share API failed, using clipboard:', error);
copyTrackUrlToClipboard(url, shareText);
}
});
} else {
// canShare returned false, use clipboard
console.log('đ Share not available, using clipboard');
copyTrackUrlToClipboard(url, shareText);
}
} else {
// Use clipboard for desktop
copyTrackUrlToClipboard(url, shareText);
}
}
// Copy track URL to clipboard
function copyTrackUrlToClipboard(url, shareText) {
const shareData = `${shareText}\n${url}`;
// Check if we're in a secure context (HTTPS or localhost)
const isSecureContext = window.isSecureContext || location.protocol === 'https:' || location.hostname === 'localhost' || location.hostname === '127.0.0.1';
// Try modern clipboard API first (requires secure context)
if (isSecureContext && navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(shareData)
.then(() => {
console.log('đ Clipboard copy successful');
if (typeof showNotification === 'function') {
showNotification('đ Track link copied to clipboard!', 'success');
} else {
alert('Track link copied to clipboard!\n\n' + shareData);
}
})
.catch((error) => {
console.error('đ Clipboard API failed:', error);
// Fallback to execCommand method
fallbackCopyTrack(shareData);
});
} else {
// Not in secure context or clipboard API not available, use fallback
console.log('đ Clipboard API not available, using fallback');
fallbackCopyTrack(shareData);
}
}
// Fallback copy method for track sharing
function fallbackCopyTrack(shareData) {
console.log('đ Using fallback copy method for track');
// Use textarea for better mobile compatibility
const textarea = document.createElement('textarea');
textarea.value = shareData;
textarea.style.position = 'fixed';
textarea.style.left = '-9999px';
textarea.style.top = '-9999px';
textarea.style.opacity = '0';
textarea.setAttribute('readonly', '');
textarea.setAttribute('aria-hidden', 'true');
document.body.appendChild(textarea);
// For Android, we need to ensure the element is visible and focused
if (/Android/i.test(navigator.userAgent)) {
textarea.style.position = 'absolute';
textarea.style.left = '0';
textarea.style.top = '0';
textarea.style.width = '1px';
textarea.style.height = '1px';
textarea.style.opacity = '0.01';
}
// Select the text
textarea.focus();
textarea.select();
textarea.setSelectionRange(0, shareData.length);
try {
const successful = document.execCommand('copy');
// Clean up
setTimeout(() => {
document.body.removeChild(textarea);
}, 100);
if (successful) {
console.log('đ Fallback copy successful');
if (typeof showNotification === 'function') {
showNotification('đ Track link copied to clipboard!', 'success');
} else {
alert('Track link copied to clipboard!\n\n' + shareData);
}
} else {
console.log('đ Fallback copy failed, showing prompt');
// Show a more user-friendly prompt
const userCopied = confirm('Please copy this link:\n\n' + shareData + '\n\nClick OK if you copied it.');
if (userCopied && typeof showNotification === 'function') {
showNotification('đ Link copied!', 'success');
}
}
} catch (error) {
console.error('đ Fallback copy error:', error);
document.body.removeChild(textarea);
// Show a more user-friendly prompt
const userCopied = confirm('Please copy this link:\n\n' + shareData + '\n\nClick OK if you copied it.');
if (userCopied && typeof showNotification === 'function') {
showNotification('đ Link copied!', 'success');
}
}
}
// Share Community Feed
function shareCommunityFeed() {
console.log('Share Community Feed button clicked');
const urlToCopy = window.location.href;
console.log('URL to copy:', urlToCopy);
// Try native share API first (mobile devices)
if (navigator.share) {
navigator.share({
title: 'Community Feed - SoundStudioPro',
text: 'Check out the community feed on SoundStudioPro!',
url: urlToCopy
})
.then(() => {
console.log('Share successful');
if (typeof showNotification === 'function') {
showNotification('â
Shared successfully!', 'success');
}
})
.catch((error) => {
// User cancelled or error - fallback to clipboard
if (error.name !== 'AbortError') {
console.log('Share API failed, using clipboard:', error);
copyCommunityUrlToClipboard(urlToCopy);
}
});
} else {
// Use clipboard for desktop
copyCommunityUrlToClipboard(urlToCopy);
}
}
// Copy community URL to clipboard
function copyCommunityUrlToClipboard(urlToCopy) {
// Try modern clipboard API first
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(urlToCopy)
.then(() => {
console.log('Clipboard copy successful');
if (typeof showNotification === 'function') {
showNotification('đ Community Feed link copied to clipboard!', 'success');
} else {
alert('Link copied to clipboard: ' + urlToCopy);
}
})
.catch((error) => {
console.error('Clipboard API failed:', error);
fallbackShareCommunity(urlToCopy);
});
} else {
fallbackShareCommunity(urlToCopy);
}
}
// Fallback share method for community feed
function fallbackShareCommunity(urlToCopy) {
console.log('Using fallback share method');
const input = document.createElement('input');
input.style.position = 'fixed';
input.style.left = '-9999px';
input.style.top = '-9999px';
input.value = urlToCopy;
document.body.appendChild(input);
input.focus();
input.select();
input.setSelectionRange(0, 99999); // For mobile devices
try {
const successful = document.execCommand('copy');
document.body.removeChild(input);
if (successful) {
console.log('Fallback copy successful');
if (typeof showNotification === 'function') {
showNotification('đ Community Feed link copied to clipboard!', 'success');
} else {
alert('Link copied to clipboard: ' + urlToCopy);
}
} else {
console.log('Fallback copy failed, showing prompt');
prompt('Copy this link:', urlToCopy);
}
} catch (error) {
console.error('Fallback copy error:', error);
document.body.removeChild(input);
prompt('Copy this link:', urlToCopy);
}
}
function showVariations(trackId, button) {
const container = document.getElementById(`variations-${trackId}`);
if (!container) return;
const isVisible = container.style.display !== 'none';
container.style.display = isVisible ? 'none' : 'block';
if (button) {
button.classList.toggle('active');
}
}
function addToCart(trackId, title, price, button) {
console.log('đ Adding to cart:', { trackId, title, price, button });
// Store original button HTML and disable button to prevent double-clicks
let originalHTML = null;
if (button) {
originalHTML = button.innerHTML;
button.disabled = true;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> <span>Adding...</span>';
}
// Use FormData to match cart.php expectations
const formData = new FormData();
formData.append('track_id', trackId);
formData.append('action', 'add');
formData.append('artist_plan', 'free'); // Default to free plan
fetch('/cart.php', {
method: 'POST',
body: formData
})
.then(response => {
console.log('đ Response status:', response.status, response.statusText);
console.log('đ Response headers:', response.headers.get('content-type'));
// Check if response is OK
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// Check if response is JSON
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
return response.text().then(text => {
console.error('đ Non-JSON response:', text);
throw new Error('Server returned non-JSON response');
});
}
return response.json();
})
.then(data => {
console.log('đ Cart response data:', data);
if (data.success) {
// Visual feedback
if (button) {
button.classList.add('added');
button.innerHTML = '<i class="fas fa-check"></i> <span>Added!</span>';
setTimeout(() => {
button.classList.remove('added');
if (originalHTML) {
button.innerHTML = originalHTML;
}
button.disabled = false;
}, 2000);
}
// Show notification if available
if (typeof showNotification === 'function') {
showNotification('đ "' + title + '" added to cart!', 'success');
}
// Update cart counter if it exists
const cartCounts = document.querySelectorAll('.cart-count, .cart-counter');
if (cartCounts.length > 0 && data.cart_count !== undefined) {
cartCounts.forEach(count => {
count.textContent = data.cart_count;
if (data.cart_count > 0) {
count.style.display = 'flex';
if (count.classList.contains('cart-counter')) {
count.style.display = 'block';
}
} else {
count.style.display = 'none';
}
});
} else if (cartCounts.length > 0) {
// Fallback: manually increment if API didn't return count
cartCounts.forEach(count => {
const currentCount = parseInt(count.textContent) || 0;
count.textContent = currentCount + 1;
count.style.display = 'flex';
if (count.classList.contains('cart-counter')) {
count.style.display = 'block';
}
});
}
// Refresh cart modal if it's currently open
const cartModal = document.getElementById('cartModal');
if (cartModal && cartModal.style.display === 'flex') {
if (typeof refreshCartModal === 'function') {
refreshCartModal();
}
}
} else {
// Re-enable button on error
if (button && originalHTML) {
button.disabled = false;
button.innerHTML = originalHTML;
}
// Special handling for "already in cart" - show as warning, not error
const errorMsg = data.message || data.error || 'Failed to add to cart. Please try again.';
if (data.already_in_cart) {
if (typeof showNotification === 'function') {
showNotification('âšī¸ ' + errorMsg, 'warning');
} else {
alert(errorMsg);
}
} else {
if (typeof showNotification === 'function') {
showNotification('â Error: ' + errorMsg, 'error');
} else {
alert('Error: ' + errorMsg);
}
}
}
})
.catch(error => {
console.error('â Cart error:', error);
// Re-enable button on error
if (button && originalHTML) {
button.disabled = false;
button.innerHTML = originalHTML;
}
const errorMsg = 'An error occurred while adding to cart. Please try again.';
if (typeof showNotification === 'function') {
showNotification(errorMsg, 'error');
} else {
alert(errorMsg);
}
});
}
function downloadTrack(trackId) {
window.location.href = `/api/download_track.php?id=${trackId}`;
}
// Store our playTrack function before footer overwrites it
const communityFixedPlayTrack = window.playTrack;
// Ensure playTrack function works after AJAX page loads and restore it after footer loads
document.addEventListener('DOMContentLoaded', function() {
console.log('đĩ Community Fixed page loaded - playTrack function ready');
console.log('đĩ Checking global player availability...');
console.log('window.waitForGlobalPlayer:', typeof window.waitForGlobalPlayer);
console.log('window.enhancedGlobalPlayer:', typeof window.enhancedGlobalPlayer);
console.log('window.globalPlayerReady:', window.globalPlayerReady);
// Restore our function after footer loads (footer loads at end of body)
setTimeout(function() {
if (communityFixedPlayTrack) {
window.playTrack = communityFixedPlayTrack;
console.log('đĩ Restored community_fixed playTrack function after footer load');
}
// Verify global player is ready
if (window.waitForGlobalPlayer) {
window.waitForGlobalPlayer(function() {
console.log('â
Global player confirmed ready on page load');
});
} else {
console.error('â waitForGlobalPlayer still not available after footer load');
}
}, 200);
});
// Listen for AJAX page loads
document.addEventListener('ajaxPageLoaded', function() {
console.log('đĩ Community Fixed page loaded via AJAX - playTrack function ready');
// Ensure global player is available
if (window.waitForGlobalPlayer) {
window.waitForGlobalPlayer(function() {
console.log('đĩ Global player ready after AJAX load');
});
}
});
</script>
<!-- Infinite Scroll Implementation -->
<script>
(function() {
let isLoading = false;
let currentPage = 1;
let hasMore = true;
const currentUserId = <?= $user_id ?? 0 ?>;
const tracksGrid = document.getElementById('tracksGrid');
const loader = document.getElementById('infiniteScrollLoader');
const endOfResults = document.getElementById('endOfResults');
if (!tracksGrid) return;
// Get initial state from data attributes
currentPage = parseInt(tracksGrid.getAttribute('data-current-page')) || 1;
hasMore = tracksGrid.getAttribute('data-has-more') === 'true';
// Build community playlist from all visible tracks on the page
function buildCommunityPlaylist() {
const playlist = [];
const grid = document.getElementById('tracksGrid');
if (!grid) {
console.warn('đĩ tracksGrid not found');
return playlist;
}
const trackCards = grid.querySelectorAll('.track-card[data-track-id]');
trackCards.forEach(card => {
const trackId = parseInt(card.getAttribute('data-track-id'));
const playBtn = card.querySelector('.play-btn');
if (playBtn) {
const audioUrl = playBtn.getAttribute('data-audio-url');
const title = playBtn.getAttribute('data-title');
const artist = playBtn.getAttribute('data-artist');
// Only add if we have valid data
if (audioUrl && audioUrl !== 'null' && audioUrl !== '' && title) {
// Convert relative URL to absolute if needed
let finalAudioUrl = audioUrl;
if (audioUrl && !audioUrl.startsWith('http') && !audioUrl.startsWith('//')) {
if (audioUrl.startsWith('/')) {
finalAudioUrl = window.location.origin + audioUrl;
} else {
finalAudioUrl = window.location.origin + '/' + audioUrl;
}
}
playlist.push({
id: trackId,
audio_url: finalAudioUrl,
title: title,
artist_name: artist || 'Unknown Artist',
user_id: currentUserId || 0
});
}
}
});
console.log('đĩ Built community playlist:', playlist.length, 'tracks');
return playlist;
}
// Make function globally accessible
window.buildCommunityPlaylist = buildCommunityPlaylist;
// Get current filter values
function getFilterParams() {
return {
sort: document.getElementById('sortSelect')?.value || '<?= $sort ?>',
time: document.getElementById('timeSelect')?.value || '<?= $time_filter ?>',
genre: document.getElementById('genreSelect')?.value || '<?= $genre ?>',
search: document.getElementById('searchInput')?.value.trim() || '<?= htmlspecialchars($search, ENT_QUOTES) ?>'
};
}
// Load more tracks
async function loadMoreTracks() {
if (isLoading || !hasMore) return;
isLoading = true;
loader.style.display = 'block';
const params = getFilterParams();
const queryParams = new URLSearchParams({
page: currentPage + 1,
per_page: 24,
sort: params.sort,
time: params.time,
genre: params.genre,
...(params.search ? { search: params.search } : {})
});
try {
const response = await fetch(`/api/get_community_fixed_tracks.php?${queryParams}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (!data.success) {
console.error('API returned error:', data);
throw new Error(data.error || data.message || 'Failed to load tracks');
}
if (data.success && data.tracks && data.tracks.length > 0) {
// Render new tracks
const fragment = document.createDocumentFragment();
data.tracks.forEach(track => {
const trackCard = createTrackCard(track);
fragment.appendChild(trackCard);
});
tracksGrid.appendChild(fragment);
// Update state
currentPage = data.pagination.page;
hasMore = data.pagination.has_more;
tracksGrid.setAttribute('data-current-page', currentPage);
tracksGrid.setAttribute('data-has-more', hasMore ? 'true' : 'false');
// Re-attach play button listeners for new tracks
attachPlayButtonListeners();
// Update playlist if one is currently active (so new tracks are included)
if (window.waitForGlobalPlayer) {
window.waitForGlobalPlayer(function() {
if (window.enhancedGlobalPlayer && window._communityPlaylistType === 'community_fixed') {
const buildFn = window.buildCommunityPlaylist || buildCommunityPlaylist;
const updatedPlaylist = buildFn ? buildFn() : [];
if (updatedPlaylist.length > 0) {
const currentIndex = window._communityTrackIndex || 0;
if (typeof window.enhancedGlobalPlayer.loadPagePlaylist === 'function') {
window.enhancedGlobalPlayer.loadPagePlaylist(updatedPlaylist, 'community_fixed', currentIndex);
console.log('đĩ Updated playlist with new tracks, total:', updatedPlaylist.length);
} else {
window._communityPlaylist = updatedPlaylist;
console.log('đĩ Updated window playlist with new tracks, total:', updatedPlaylist.length);
}
}
}
});
}
// Show end message if no more tracks
if (!hasMore) {
endOfResults.style.display = 'block';
}
} else {
hasMore = false;
endOfResults.style.display = 'block';
}
} catch (error) {
console.error('Error loading more tracks:', error);
console.error('Error details:', {
message: error.message,
stack: error.stack,
response: error.response
});
// Try to get more details from response if available
if (error.response) {
error.response.json().then(data => {
console.error('API Error Response:', data);
}).catch(() => {});
}
if (typeof showNotification === 'function') {
showNotification('Error loading more tracks. Please try again.', 'error');
} else {
alert('Error loading more tracks. Please try again.');
}
} finally {
isLoading = false;
loader.style.display = 'none';
}
}
// Create track card HTML (simplified version matching the PHP template)
function createTrackCard(track) {
const card = document.createElement('div');
card.className = 'track-card';
card.setAttribute('data-track-id', track.id);
card.setAttribute('itemscope', '');
card.setAttribute('itemtype', 'http://schema.org/MusicRecording');
const displayTitle = track.title || 'Untitled Track';
const duration = track.duration ? formatDuration(track.duration) : '0:00';
// Use resolved_image_url from API (which handles all fallbacks) or fallback to default
const imageUrl = track.resolved_image_url || track.image_url || '/assets/images/default-track.jpg';
const audioUrl = track.signed_audio_url || '';
const isWishlisted = track.is_in_wishlist > 0;
const displayPrice = track.price && track.price > 0 ? track.price : 1.99;
// Extract genre from metadata
let genre = 'Electronic';
let mood = null;
if (track.metadata) {
const metadata = typeof track.metadata === 'string' ? JSON.parse(track.metadata) : track.metadata;
if (metadata.genre) genre = metadata.genre;
else if (metadata.style) genre = metadata.style;
if (metadata.mood) mood = metadata.mood;
}
if ((genre === 'Electronic' || !genre) && track.genre) {
genre = track.genre;
}
const createdDate = track.created_at ? new Date(track.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '';
card.innerHTML = `
<div class="track-image-wrapper">
<img src="${escapeHtml(imageUrl)}" alt="${escapeHtml(displayTitle)}" class="track-image" loading="lazy">
<div class="track-overlay">
<button class="play-btn"
data-track-id="${track.id}"
data-audio-url="${escapeHtml(audioUrl)}"
data-title="${escapeHtml(displayTitle)}"
data-artist="${escapeHtml(track.artist_name || '')}">
<i class="fas fa-play"></i>
</button>
<div class="track-duration">${duration}</div>
</div>
</div>
<div class="track-info">
<div class="track-title-row">
<a href="/track.php?id=${track.id}" class="track-title" title="${escapeHtml(displayTitle)}" itemprop="name">${escapeHtml(displayTitle)}</a>
${track.price > 0 ? `<span class="for-sale-badge" title="Available for purchase"><i class="fas fa-tag"></i><span>For Sale</span></span>` : ''}
</div>
<a href="/artist_profile.php?id=${track.artist_id}" class="track-artist" itemprop="byArtist" itemscope itemtype="http://schema.org/MusicGroup">
<span itemprop="name">${escapeHtml(track.artist_name || 'Unknown Artist')}</span>
</a>
<div class="track-genre">
<a href="?genre=${encodeURIComponent(genre)}" class="genre-tag-link">${escapeHtml(genre)}</a>
${mood && mood !== genre && mood.toLowerCase() !== 'neutral' ? `<a href="?genre=${encodeURIComponent(mood)}" class="genre-tag-link">${escapeHtml(mood)}</a>` : ''}
</div>
${createdDate ? `<div class="track-date"><i class="fas fa-calendar-alt"></i><span>${createdDate}</span></div>` : ''}
</div>
<div class="track-stats">
<button class="stat-btn" title="Plays">
<i class="fas fa-headphones-alt"></i>
<span>${formatNumber(track.play_count || 0)}</span>
</button>
<button class="stat-btn like-btn ${track.user_liked > 0 ? 'liked' : ''}" onclick="toggleLike(${track.id}, this)">
<i class="fas fa-heart"></i>
<span>${formatNumber(track.like_count || 0)}</span>
</button>
<button class="stat-btn" onclick="showComments(${track.id})">
<i class="fas fa-comment"></i>
<span>${formatNumber(track.comment_count || 0)}</span>
</button>
<button class="stat-btn" onclick="shareTrack(${track.id})">
<i class="fas fa-share"></i>
<span>${formatNumber(track.share_count || 0)}</span>
</button>
${track.variations && track.variations.length > 0 ? `
<button class="stat-btn variations-btn" onclick="showVariations(${track.id}, this)" title="${track.variations.length} variations">
<i class="fas fa-layer-group"></i>
<span>${track.variations.length}</span>
</button>
` : ''}
${track.user_id && track.user_id != currentUserId ? `
<button class="stat-btn follow-btn ${track.is_following > 0 ? 'following' : ''}" onclick="toggleFollow(${track.user_id}, this)">
<i class="fas fa-user-plus"></i>
</button>
` : ''}
</div>
<div class="track-cart-section">
<button class="add-to-cart-btn" type="button" data-track-id="${track.id}" data-track-title="${escapeHtml(displayTitle)}" data-track-price="${displayPrice}" onclick="addToCart(${track.id}, '${escapeHtml(displayTitle)}', ${displayPrice}, this)">
<i class="fas fa-shopping-cart"></i>
<span>Add to Cart</span>
<span class="cart-price">${displayPrice > 0 ? '$' + displayPrice.toFixed(2) : 'Free'}</span>
</button>
<button class="wishlist-inline-btn ${isWishlisted ? 'active' : ''}" onclick="toggleWishlist(${track.id}, this)" aria-pressed="${isWishlisted}">
<i class="${isWishlisted ? 'fas' : 'far'} fa-heart"></i>
</button>
</div>
`;
return card;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function formatDuration(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
function formatNumber(num) {
return parseInt(num).toLocaleString();
}
// Attach play button listeners (matches original behavior with pause support)
function attachPlayButtonListeners() {
const playButtons = tracksGrid.querySelectorAll('.play-btn:not([data-listener-attached])');
playButtons.forEach(button => {
button.setAttribute('data-listener-attached', 'true');
button.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
const trackId = this.getAttribute('data-track-id');
const audioUrl = this.getAttribute('data-audio-url');
const title = this.getAttribute('data-title');
const artist = this.getAttribute('data-artist');
const isCurrentlyPlaying = this.classList.contains('playing');
console.log('đĩ Infinite scroll play button clicked:', { trackId, audioUrl, title, artist, isCurrentlyPlaying });
// Validate audio URL
if (!audioUrl || audioUrl === 'null' || audioUrl === 'NULL' || audioUrl === '') {
console.error('â Invalid audio URL:', audioUrl);
alert('This track is not available for playback.');
return;
}
// If button is already in playing state, pause instead
if (isCurrentlyPlaying) {
if (window.waitForGlobalPlayer) {
window.waitForGlobalPlayer(function() {
if (window.enhancedGlobalPlayer) {
// Check if global player has togglePlayPause or pause method
if (typeof window.enhancedGlobalPlayer.togglePlayPause === 'function') {
console.log('đĩ Pausing via togglePlayPause');
window.enhancedGlobalPlayer.togglePlayPause();
} else if (typeof window.enhancedGlobalPlayer.pause === 'function') {
console.log('đĩ Pausing via pause method');
window.enhancedGlobalPlayer.pause();
}
// Update button state
button.classList.remove('playing');
const icon = button.querySelector('i');
if (icon) icon.className = 'fas fa-play';
// Clear other playing states
document.querySelectorAll('.play-btn').forEach(btn => {
if (btn !== button) {
btn.classList.remove('playing');
const btnIcon = btn.querySelector('i');
if (btnIcon) btnIcon.className = 'fas fa-play';
}
});
}
});
}
return;
}
// Clear other playing states
document.querySelectorAll('.play-btn').forEach(btn => {
btn.classList.remove('playing');
const icon = btn.querySelector('i');
if (icon) icon.className = 'fas fa-play';
});
// Set this button as playing
this.classList.add('playing');
const icon = this.querySelector('i');
if (icon) icon.className = 'fas fa-pause';
// Play track using global player
// Ensure audio URL is absolute if it's relative
let finalAudioUrl = audioUrl;
if (audioUrl && !audioUrl.startsWith('http') && !audioUrl.startsWith('//')) {
if (audioUrl.startsWith('/')) {
finalAudioUrl = window.location.origin + audioUrl;
} else {
finalAudioUrl = window.location.origin + '/' + audioUrl;
}
console.log('đĩ Converted relative URL to absolute:', finalAudioUrl);
}
if (!finalAudioUrl || finalAudioUrl.trim() === '') {
console.error('â Audio URL is empty!');
alert('Audio file not available.');
button.classList.remove('playing');
if (icon) icon.className = 'fas fa-play';
return;
}
if (window.waitForGlobalPlayer) {
window.waitForGlobalPlayer(function() {
if (window.enhancedGlobalPlayer && typeof window.enhancedGlobalPlayer.playTrack === 'function') {
try {
// Build and load playlist before playing
const buildFn = window.buildCommunityPlaylist || buildCommunityPlaylist;
const communityPlaylist = buildFn ? buildFn() : [];
if (communityPlaylist.length > 0) {
// Find the index of the clicked track
const clickedTrackId = parseInt(trackId);
const trackIndex = communityPlaylist.findIndex(t => t.id === clickedTrackId);
const startIndex = trackIndex !== -1 ? trackIndex : 0;
// Load playlist into global player
if (typeof window.enhancedGlobalPlayer.loadPagePlaylist === 'function') {
window.enhancedGlobalPlayer.loadPagePlaylist(communityPlaylist, 'community_fixed', startIndex);
console.log('đĩ Community: Playlist loaded via loadPagePlaylist, index:', startIndex, 'of', communityPlaylist.length);
} else {
// Fallback to window storage
window._communityPlaylist = communityPlaylist;
window._communityPlaylistType = 'community_fixed';
window._communityTrackIndex = startIndex;
console.log('đĩ Community: Playlist stored on window, index:', startIndex, 'of', communityPlaylist.length);
}
}
console.log('đĩ Playing track:', { finalAudioUrl, title, artist, trackId });
const success = window.enhancedGlobalPlayer.playTrack(finalAudioUrl, title, artist, trackId);
if (success) {
console.log('â
Track playing successfully');
if (trackId) recordTrackPlay(trackId);
} else {
console.error('â playTrack returned false');
alert('Failed to play track.');
// Reset button
button.classList.remove('playing');
if (icon) icon.className = 'fas fa-play';
}
} catch (error) {
console.error('â Error:', error);
alert('Error: ' + error.message);
// Reset button
button.classList.remove('playing');
if (icon) icon.className = 'fas fa-play';
}
} else {
console.error('â Global player not available');
alert('Player not ready. Please refresh.');
// Reset button
button.classList.remove('playing');
if (icon) icon.className = 'fas fa-play';
}
});
} else {
// Fallback
setTimeout(function() {
if (window.enhancedGlobalPlayer && typeof window.enhancedGlobalPlayer.playTrack === 'function') {
// Build and load playlist
const communityPlaylist = buildCommunityPlaylist();
if (communityPlaylist.length > 0) {
const clickedTrackId = parseInt(trackId);
const trackIndex = communityPlaylist.findIndex(t => t.id === clickedTrackId);
const startIndex = trackIndex !== -1 ? trackIndex : 0;
if (typeof window.enhancedGlobalPlayer.loadPagePlaylist === 'function') {
window.enhancedGlobalPlayer.loadPagePlaylist(communityPlaylist, 'community_fixed', startIndex);
} else {
window._communityPlaylist = communityPlaylist;
window._communityPlaylistType = 'community_fixed';
window._communityTrackIndex = startIndex;
}
}
window.enhancedGlobalPlayer.playTrack(finalAudioUrl, title, artist, trackId);
if (trackId) recordTrackPlay(trackId);
} else {
alert('Audio player not ready. Please refresh the page.');
button.classList.remove('playing');
if (icon) icon.className = 'fas fa-play';
}
}, 500);
}
});
});
}
// Scroll detection
function handleScroll() {
if (isLoading || !hasMore) return;
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const windowHeight = window.innerHeight;
const documentHeight = document.documentElement.scrollHeight;
// Load more when user is 300px from bottom
if (scrollTop + windowHeight >= documentHeight - 300) {
loadMoreTracks();
}
}
// Throttle scroll events
let scrollTimeout;
window.addEventListener('scroll', function() {
if (scrollTimeout) {
clearTimeout(scrollTimeout);
}
scrollTimeout = setTimeout(handleScroll, 100);
});
// Reset on filter change
const originalUpdateFilters = window.updateFilters;
window.updateFilters = function() {
// Reset infinite scroll state
currentPage = 1;
hasMore = true;
tracksGrid.setAttribute('data-current-page', '1');
tracksGrid.setAttribute('data-has-more', 'true');
loader.style.display = 'none';
endOfResults.style.display = 'none';
// Call original function
if (originalUpdateFilters) {
originalUpdateFilters();
}
};
})();
</script>
<?php include 'includes/footer.php'; ?>
<!-- Play button event listeners - like other working pages -->
<script>
// Setup play button event listeners
document.addEventListener('DOMContentLoaded', function() {
console.log('đĩ Setting up play button listeners');
// Main track play buttons
const playButtons = document.querySelectorAll('.play-btn, .variation-play-btn');
console.log('đĩ Found', playButtons.length, 'play buttons');
playButtons.forEach(button => {
button.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
const trackId = this.getAttribute('data-track-id');
const audioUrl = this.getAttribute('data-audio-url');
const title = this.getAttribute('data-title');
const artist = this.getAttribute('data-artist');
const isCurrentlyPlaying = this.classList.contains('playing');
console.log('đĩ Play button clicked:', { trackId, audioUrl, title, artist, isCurrentlyPlaying });
// Validate
if (!audioUrl || audioUrl === 'null' || audioUrl === 'NULL' || audioUrl === '') {
console.error('â Invalid audio URL:', audioUrl);
alert('This track is not available for playback.');
return;
}
// If button is already in playing state, pause instead
if (isCurrentlyPlaying) {
if (window.waitForGlobalPlayer) {
window.waitForGlobalPlayer(function() {
if (window.enhancedGlobalPlayer) {
// Check if global player has togglePlayPause or pause method
if (typeof window.enhancedGlobalPlayer.togglePlayPause === 'function') {
console.log('đĩ Pausing via togglePlayPause');
window.enhancedGlobalPlayer.togglePlayPause();
} else if (typeof window.enhancedGlobalPlayer.pause === 'function') {
console.log('đĩ Pausing via pause method');
window.enhancedGlobalPlayer.pause();
}
// Update button state
button.classList.remove('playing');
const icon = button.querySelector('i');
if (icon) icon.className = 'fas fa-play';
// Clear other playing states
playButtons.forEach(btn => {
if (btn !== button) {
btn.classList.remove('playing');
const btnIcon = btn.querySelector('i');
if (btnIcon) btnIcon.className = 'fas fa-play';
}
});
}
});
}
return;
}
// Clear other playing states
playButtons.forEach(btn => {
btn.classList.remove('playing');
const icon = btn.querySelector('i');
if (icon) icon.className = 'fas fa-play';
});
// Set this button as playing
this.classList.add('playing');
const icon = this.querySelector('i');
if (icon) icon.className = 'fas fa-pause';
// Play track using global player
// Ensure audio URL is absolute if it's relative
let finalAudioUrl = audioUrl;
if (audioUrl && !audioUrl.startsWith('http') && !audioUrl.startsWith('//')) {
if (audioUrl.startsWith('/')) {
finalAudioUrl = window.location.origin + audioUrl;
} else {
finalAudioUrl = window.location.origin + '/' + audioUrl;
}
console.log('đĩ Converted relative URL to absolute:', finalAudioUrl);
}
if (!finalAudioUrl || finalAudioUrl.trim() === '') {
console.error('â Audio URL is empty!');
alert('Audio file not available.');
button.classList.remove('playing');
if (icon) icon.className = 'fas fa-play';
return;
}
if (window.waitForGlobalPlayer) {
window.waitForGlobalPlayer(function() {
if (window.enhancedGlobalPlayer && typeof window.enhancedGlobalPlayer.playTrack === 'function') {
try {
// Build and load playlist before playing
const buildFn = window.buildCommunityPlaylist || buildCommunityPlaylist;
const communityPlaylist = buildFn ? buildFn() : [];
if (communityPlaylist.length > 0) {
// Find the index of the clicked track
const clickedTrackId = parseInt(trackId);
const trackIndex = communityPlaylist.findIndex(t => t.id === clickedTrackId);
const startIndex = trackIndex !== -1 ? trackIndex : 0;
// Load playlist into global player
if (typeof window.enhancedGlobalPlayer.loadPagePlaylist === 'function') {
window.enhancedGlobalPlayer.loadPagePlaylist(communityPlaylist, 'community_fixed', startIndex);
console.log('đĩ Community: Playlist loaded via loadPagePlaylist, index:', startIndex, 'of', communityPlaylist.length);
} else {
// Fallback to window storage
window._communityPlaylist = communityPlaylist;
window._communityPlaylistType = 'community_fixed';
window._communityTrackIndex = startIndex;
console.log('đĩ Community: Playlist stored on window, index:', startIndex, 'of', communityPlaylist.length);
}
}
console.log('đĩ Playing track:', { finalAudioUrl, title, artist, trackId });
const success = window.enhancedGlobalPlayer.playTrack(finalAudioUrl, title, artist, trackId);
if (success) {
console.log('â
Track playing successfully');
if (trackId) recordTrackPlay(trackId);
} else {
console.error('â playTrack returned false');
alert('Failed to play track.');
// Reset button
button.classList.remove('playing');
if (icon) icon.className = 'fas fa-play';
}
} catch (error) {
console.error('â Error:', error);
alert('Error: ' + error.message);
// Reset button
button.classList.remove('playing');
if (icon) icon.className = 'fas fa-play';
}
} else {
console.error('â Global player not available');
alert('Player not ready. Please refresh.');
// Reset button
button.classList.remove('playing');
if (icon) icon.className = 'fas fa-play';
}
});
} else {
// Fallback
setTimeout(function() {
if (window.enhancedGlobalPlayer && typeof window.enhancedGlobalPlayer.playTrack === 'function') {
// Build and load playlist
const buildFn = window.buildCommunityPlaylist || buildCommunityPlaylist;
const communityPlaylist = buildFn();
if (communityPlaylist.length > 0) {
const clickedTrackId = parseInt(trackId);
const trackIndex = communityPlaylist.findIndex(t => t.id === clickedTrackId);
const startIndex = trackIndex !== -1 ? trackIndex : 0;
if (typeof window.enhancedGlobalPlayer.loadPagePlaylist === 'function') {
window.enhancedGlobalPlayer.loadPagePlaylist(communityPlaylist, 'community_fixed', startIndex);
} else {
window._communityPlaylist = communityPlaylist;
window._communityPlaylistType = 'community_fixed';
window._communityTrackIndex = startIndex;
}
}
window.enhancedGlobalPlayer.playTrack(finalAudioUrl, title, artist, trackId);
if (trackId) recordTrackPlay(trackId);
} else {
alert('Audio player not ready. Please refresh the page.');
button.classList.remove('playing');
if (icon) icon.className = 'fas fa-play';
}
}, 500);
}
});
});
console.log('â
Play button listeners setup complete');
});
</script>