![]() 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
/**
* Crates Browser - Search and discover public crates
*/
session_start();
require_once 'config/database.php';
require_once 'includes/translations.php';
$pdo = getDBConnection();
// Search and filter parameters
$search = isset($_GET['q']) ? trim($_GET['q']) : '';
$artist_filter = isset($_GET['artist']) ? (int)$_GET['artist'] : 0;
$sort = isset($_GET['sort']) ? $_GET['sort'] : 'recent';
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$per_page = 12;
$offset = ($page - 1) * $per_page;
// Build query conditions
$conditions = ["ap.is_public = 1"];
$params = [];
if ($search) {
$conditions[] = "(ap.name LIKE ? OR ap.description LIKE ? OR u.name LIKE ?)";
$searchTerm = "%{$search}%";
$params[] = $searchTerm;
$params[] = $searchTerm;
$params[] = $searchTerm;
}
if ($artist_filter) {
$conditions[] = "ap.user_id = ?";
$params[] = $artist_filter;
}
$whereClause = implode(' AND ', $conditions);
// Sorting
$orderBy = match($sort) {
'popular' => 'track_count DESC',
'duration' => 'total_duration DESC',
'name' => 'ap.name ASC',
default => 'ap.updated_at DESC'
};
// Check if is_description_public column exists
$checkDescCol = $pdo->query("SHOW COLUMNS FROM artist_playlists LIKE 'is_description_public'");
$hasDescPublicColumn = $checkDescCol->rowCount() > 0;
// Get total count
$countQuery = "
SELECT COUNT(DISTINCT ap.id) as total
FROM artist_playlists ap
JOIN users u ON ap.user_id = u.id
LEFT JOIN playlist_tracks pt ON ap.id = pt.playlist_id
LEFT JOIN music_tracks mt ON pt.track_id = mt.id AND mt.status = 'complete'
WHERE $whereClause
GROUP BY ap.id
HAVING COUNT(DISTINCT pt.track_id) > 0
";
$stmt = $pdo->prepare("SELECT COUNT(*) FROM ($countQuery) as subq");
$stmt->execute($params);
$total_crates = $stmt->fetchColumn();
$total_pages = ceil($total_crates / $per_page);
// Get crates
$query = "
SELECT
ap.id,
ap.name,
ap.description,
ap.created_at,
ap.updated_at,
" . ($hasDescPublicColumn ? "ap.is_description_public," : "1 as is_description_public,") . "
COUNT(DISTINCT pt.track_id) as track_count,
COALESCE(SUM(mt.duration), 0) as total_duration,
u.name as artist_name,
u.id as artist_id,
up.profile_image
FROM artist_playlists ap
JOIN users u ON ap.user_id = u.id
LEFT JOIN user_profiles up ON u.id = up.user_id
LEFT JOIN playlist_tracks pt ON ap.id = pt.playlist_id
LEFT JOIN music_tracks mt ON pt.track_id = mt.id AND mt.status = 'complete'
WHERE $whereClause
GROUP BY ap.id, ap.name, ap.description, ap.created_at, ap.updated_at, u.name, u.id, up.profile_image
HAVING track_count > 0
ORDER BY $orderBy
LIMIT $per_page OFFSET $offset
";
$stmt = $pdo->prepare($query);
$stmt->execute($params);
$crates = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Get popular artists for filter
$artistsQuery = "
SELECT DISTINCT u.id, u.name, COUNT(DISTINCT ap.id) as crate_count
FROM users u
JOIN artist_playlists ap ON u.id = ap.user_id AND ap.is_public = 1
JOIN playlist_tracks pt ON ap.id = pt.playlist_id
GROUP BY u.id, u.name
HAVING crate_count > 0
ORDER BY crate_count DESC
LIMIT 20
";
$popular_artists = $pdo->query($artistsQuery)->fetchAll(PDO::FETCH_ASSOC);
// Format duration helper
function formatCrateDuration($seconds) {
$hours = floor($seconds / 3600);
$minutes = floor(($seconds % 3600) / 60);
if ($hours > 0) {
return sprintf('%dh %dm', $hours, $minutes);
}
return sprintf('%dm', $minutes);
}
include 'includes/header.php';
?>
<style>
/* Crates Browser Styles */
.crates-page {
min-height: 100vh;
background: linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 50%, #16213e 100%);
padding: 2rem 0;
}
.crates-container {
max-width: 1400px;
margin: 0 auto;
padding: 0 2rem;
}
/* Header */
.crates-header {
text-align: center;
margin-bottom: 3rem;
}
.crates-header h1 {
font-size: 3rem;
font-weight: 800;
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.5rem;
}
.crates-header p {
color: #a0aec0;
font-size: 1.1rem;
}
/* Search & Filters */
.crates-filters {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
padding: 1.5rem;
margin-bottom: 2rem;
backdrop-filter: blur(10px);
}
.filters-row {
display: flex;
gap: 1rem;
flex-wrap: wrap;
align-items: center;
}
.search-box {
flex: 1;
min-width: 250px;
position: relative;
}
.search-box input {
width: 100%;
padding: 0.875rem 1rem 0.875rem 3rem;
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 10px;
background: rgba(255, 255, 255, 0.05);
color: white;
font-size: 1rem;
transition: all 0.3s ease;
}
.search-box input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
}
.search-box input::placeholder {
color: #6b7280;
}
.search-box i {
position: absolute;
left: 1rem;
top: 50%;
transform: translateY(-50%);
color: #6b7280;
}
.filter-select {
padding: 0.875rem 1rem;
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 10px;
background: rgba(255, 255, 255, 0.05);
color: white;
font-size: 0.95rem;
cursor: pointer;
min-width: 150px;
}
.filter-select option {
background: #1a1a2e;
color: white;
}
.search-submit-btn {
flex-shrink: 0;
white-space: nowrap;
min-width: auto;
}
.search-submit-btn .search-btn-text {
display: inline;
}
/* Stats Bar */
.stats-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
padding: 0 0.5rem;
}
.stats-bar .count {
color: #a0aec0;
font-size: 0.95rem;
}
.stats-bar .count strong {
color: #667eea;
}
/* Crates Grid */
.crates-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 2rem;
}
/* Crate Card - Redesigned */
.crate-card {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 20px;
overflow: hidden;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
backdrop-filter: blur(10px);
}
.crate-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #667eea, #764ba2, #f093fb);
opacity: 0;
transition: opacity 0.3s ease;
}
.crate-card:hover {
transform: translateY(-8px) scale(1.02);
border-color: rgba(102, 126, 234, 0.5);
box-shadow: 0 25px 50px rgba(102, 126, 234, 0.25), 0 0 0 1px rgba(102, 126, 234, 0.1);
}
.crate-card:hover::before {
opacity: 1;
}
.crate-card-image {
position: relative;
height: 200px;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.3) 0%, rgba(118, 75, 162, 0.3) 50%, rgba(240, 147, 251, 0.3) 100%);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.crate-card-image::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(180deg, transparent 0%, rgba(0, 0, 0, 0.3) 100%);
}
.crate-icon-large {
font-size: 4rem;
color: rgba(255, 255, 255, 0.9);
z-index: 1;
position: relative;
text-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.crate-play-overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.4);
opacity: 0;
transition: opacity 0.3s ease;
z-index: 2;
}
.crate-card:hover .crate-play-overlay {
opacity: 1;
}
.crate-play-btn-large {
width: 70px;
height: 70px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea, #764ba2);
border: none;
color: white;
font-size: 1.5rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.4);
position: relative;
}
.crate-play-btn-large:hover {
transform: scale(1.1);
box-shadow: 0 12px 32px rgba(102, 126, 234, 0.6);
}
.crate-play-btn-large:active {
transform: scale(0.95);
}
.crate-play-btn-large.loading {
pointer-events: none;
}
.crate-play-btn-large.loading i {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.crate-card-content {
padding: 1.5rem;
}
.crate-title-section {
margin-bottom: 1rem;
}
.crate-title {
font-size: 1.25rem;
font-weight: 700;
color: white;
margin-bottom: 0.5rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.3;
}
.crate-title a {
color: inherit;
text-decoration: none;
transition: color 0.2s;
}
.crate-title a:hover {
color: #667eea;
}
.crate-artist {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
color: #a0aec0;
}
.crate-artist-avatar {
width: 24px;
height: 24px;
border-radius: 50%;
object-fit: cover;
border: 2px solid rgba(102, 126, 234, 0.3);
}
.crate-artist a {
color: #a0aec0;
text-decoration: none;
transition: color 0.2s;
}
.crate-artist a:hover {
color: #667eea;
}
.crate-description {
color: #a0aec0;
font-size: 0.9rem;
line-height: 1.6;
margin-bottom: 1.25rem;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
min-height: 2.7rem;
}
.crate-stats {
display: flex;
gap: 1.5rem;
margin-bottom: 1.25rem;
padding-bottom: 1.25rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.crate-stat {
display: flex;
align-items: center;
gap: 0.5rem;
color: #9ca3af;
font-size: 0.9rem;
}
.crate-stat i {
color: #667eea;
font-size: 0.95rem;
}
.crate-actions {
display: flex;
gap: 0.75rem;
}
.crate-btn {
flex: 1;
padding: 0.875rem 1rem;
border: none;
border-radius: 10px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
transition: all 0.2s ease;
text-decoration: none;
position: relative;
overflow: hidden;
}
.crate-btn::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), transparent);
opacity: 0;
transition: opacity 0.2s;
}
.crate-btn:hover::before {
opacity: 1;
}
.crate-btn.play-btn {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
flex: 1.5;
font-weight: 700;
}
.crate-btn.play-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
}
.crate-btn.play-btn:active {
transform: translateY(0);
}
.crate-btn.play-btn.loading {
pointer-events: none;
opacity: 0.7;
}
.crate-btn.secondary {
background: rgba(255, 255, 255, 0.08);
color: white;
border: 1px solid rgba(255, 255, 255, 0.15);
}
.crate-btn.secondary:hover {
background: rgba(255, 255, 255, 0.12);
transform: translateY(-1px);
}
/* Pagination */
.pagination {
display: flex;
justify-content: center;
gap: 0.5rem;
margin-top: 3rem;
}
.pagination a, .pagination span {
padding: 0.75rem 1rem;
border-radius: 8px;
color: white;
text-decoration: none;
font-size: 0.95rem;
transition: all 0.2s;
}
.pagination a {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.pagination a:hover {
background: rgba(102, 126, 234, 0.2);
border-color: rgba(102, 126, 234, 0.4);
}
.pagination .current {
background: linear-gradient(135deg, #667eea, #764ba2);
}
.pagination .disabled {
opacity: 0.5;
pointer-events: none;
}
/* Empty State */
.empty-state {
text-align: center;
padding: 4rem 2rem;
background: rgba(255, 255, 255, 0.02);
border-radius: 16px;
border: 1px dashed rgba(255, 255, 255, 0.1);
}
.empty-state i {
font-size: 4rem;
color: #667eea;
margin-bottom: 1.5rem;
opacity: 0.5;
}
.empty-state h3 {
color: white;
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.empty-state p {
color: #a0aec0;
font-size: 1rem;
}
/* Responsive */
@media (max-width: 768px) {
.crates-container {
padding: 0 1rem;
}
.crates-header {
margin-bottom: 2rem;
}
.crates-header h1 {
font-size: 2rem;
}
.crates-header p {
font-size: 1rem;
}
.crates-filters {
padding: 1rem;
margin-bottom: 1.5rem;
}
.filters-row {
flex-direction: column;
gap: 0.75rem;
}
.search-box {
width: 100%;
min-width: unset;
}
.filter-select {
width: 100%;
min-width: unset;
}
.search-submit-btn {
width: 100%;
justify-content: center;
padding: 0.875rem 1rem;
}
.search-submit-btn .search-btn-text {
display: inline;
}
.crates-grid {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.stats-bar {
flex-direction: column;
gap: 0.5rem;
text-align: center;
padding: 0;
}
.crate-card-image {
height: 180px;
}
.crate-icon-large {
font-size: 3rem;
}
.crate-play-btn-large {
width: 60px;
height: 60px;
font-size: 1.25rem;
}
.crate-card-content {
padding: 1.25rem;
}
.crate-actions {
flex-wrap: wrap;
gap: 0.5rem;
}
.crate-btn.play-btn {
flex: 1 1 100%;
margin-bottom: 0.5rem;
}
.crate-btn.secondary {
flex: 1;
min-width: 48px;
}
.pagination {
flex-wrap: wrap;
gap: 0.25rem;
margin-top: 2rem;
}
.pagination a, .pagination span {
padding: 0.5rem 0.75rem;
font-size: 0.85rem;
}
}
/* Extra small devices */
@media (max-width: 480px) {
.crates-header h1 {
font-size: 1.75rem;
}
.crate-card-image {
height: 160px;
}
.crate-icon-large {
font-size: 2.5rem;
}
.crate-play-btn-large {
width: 50px;
height: 50px;
font-size: 1rem;
}
.crate-title {
font-size: 1.1rem;
}
.crate-stats {
flex-direction: column;
gap: 0.75rem;
align-items: flex-start;
}
}
</style>
<div class="crates-page">
<div class="crates-container">
<!-- Header -->
<div class="crates-header">
<h1><i class="fas fa-box-open"></i> <?= t('crates.browse_title') ?></h1>
<p><?= t('crates.browse_subtitle') ?></p>
</div>
<!-- Filters -->
<form class="crates-filters" method="GET" action="" id="cratesSearchForm">
<div class="filters-row">
<div class="search-box">
<i class="fas fa-search"></i>
<input type="text" name="q" value="<?= htmlspecialchars($search) ?>" placeholder="<?= t('crates.search_placeholder') ?>" id="cratesSearchInput" autocomplete="off">
</div>
<select name="artist" class="filter-select" onchange="this.form.submit()">
<option value=""><?= t('crates.all_artists') ?></option>
<?php foreach ($popular_artists as $artist): ?>
<option value="<?= $artist['id'] ?>" <?= $artist_filter == $artist['id'] ? 'selected' : '' ?>>
<?= htmlspecialchars($artist['name']) ?> (<?= $artist['crate_count'] ?>)
</option>
<?php endforeach; ?>
</select>
<select name="sort" class="filter-select" onchange="this.form.submit()">
<option value="recent" <?= $sort == 'recent' ? 'selected' : '' ?>><?= t('crates.sort_recent') ?></option>
<option value="popular" <?= $sort == 'popular' ? 'selected' : '' ?>><?= t('crates.sort_popular') ?></option>
<option value="duration" <?= $sort == 'duration' ? 'selected' : '' ?>><?= t('crates.sort_duration') ?></option>
<option value="name" <?= $sort == 'name' ? 'selected' : '' ?>><?= t('crates.sort_name') ?></option>
</select>
<button type="submit" class="crate-btn primary search-submit-btn">
<i class="fas fa-search"></i> <span class="search-btn-text"><?= t('crates.search_btn') ?></span>
</button>
</div>
</form>
<!-- Stats -->
<div class="stats-bar">
<div class="count">
<?php if ($search || $artist_filter): ?>
<strong><?= $total_crates ?></strong> <?= t('crates.results_found') ?>
<?php else: ?>
<strong><?= $total_crates ?></strong> <?= t('crates.public_crates') ?>
<?php endif; ?>
</div>
</div>
<!-- Crates Grid -->
<?php if (empty($crates)): ?>
<div class="empty-state">
<i class="fas fa-box-open"></i>
<h3><?= t('crates.no_crates_found') ?></h3>
<p><?= t('crates.try_different_search') ?></p>
</div>
<?php else: ?>
<div class="crates-grid">
<?php foreach ($crates as $crate):
$duration_formatted = formatCrateDuration($crate['total_duration']);
$show_description = $crate['is_description_public'] && !empty($crate['description']);
$artist_avatar = !empty($crate['profile_image']) ? htmlspecialchars($crate['profile_image']) : '';
?>
<div class="crate-card" data-crate-id="<?= $crate['id'] ?>">
<div class="crate-card-image">
<i class="fas fa-box-open crate-icon-large"></i>
<div class="crate-play-overlay">
<button class="crate-play-btn-large" onclick="playCrateFromCard(<?= $crate['id'] ?>, this)" title="<?= t('library.crates.play_all') ?>">
<i class="fas fa-play"></i>
</button>
</div>
</div>
<div class="crate-card-content">
<div class="crate-title-section">
<div class="crate-title">
<a href="javascript:void(0)" onclick="openCrateModal(<?= $crate['id'] ?>)"><?= htmlspecialchars($crate['name']) ?></a>
</div>
<div class="crate-artist">
<?php if ($artist_avatar): ?>
<img src="<?= $artist_avatar ?>" alt="<?= htmlspecialchars($crate['artist_name']) ?>" class="crate-artist-avatar" onerror="this.style.display='none'">
<?php endif; ?>
<span><?= t('crates.by') ?> <a href="/artist/<?= $crate['artist_id'] ?>" target="_blank"><?= htmlspecialchars($crate['artist_name']) ?></a></span>
</div>
</div>
<div class="crate-description">
<?= $show_description ? htmlspecialchars($crate['description']) : '<em style="opacity: 0.5;">' . t('crates.no_description') . '</em>' ?>
</div>
<div class="crate-stats">
<div class="crate-stat">
<i class="fas fa-music"></i>
<span><?= $crate['track_count'] ?> <?= t('crates.tracks') ?></span>
</div>
<div class="crate-stat">
<i class="fas fa-clock"></i>
<span><?= $duration_formatted ?></span>
</div>
</div>
<div class="crate-actions">
<button class="crate-btn play-btn" onclick="playCrateFromCard(<?= $crate['id'] ?>, this)">
<i class="fas fa-play"></i>
<span><?= t('library.crates.play_all') ?></span>
</button>
<button class="crate-btn secondary" onclick="openCrateModal(<?= $crate['id'] ?>)" title="<?= t('crates.view') ?>">
<i class="fas fa-eye"></i>
</button>
<button class="crate-btn secondary" onclick="shareCrate(<?= $crate['id'] ?>, '<?= htmlspecialchars($crate['name'], ENT_QUOTES) ?>')" title="<?= t('library.crates.share') ?>">
<i class="fas fa-share-alt"></i>
</button>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<!-- Pagination -->
<?php if ($total_pages > 1): ?>
<div class="pagination">
<?php if ($page > 1): ?>
<a href="?<?= http_build_query(array_merge($_GET, ['page' => $page - 1])) ?>">
<i class="fas fa-chevron-left"></i>
</a>
<?php endif; ?>
<?php
$start = max(1, $page - 2);
$end = min($total_pages, $page + 2);
if ($start > 1): ?>
<a href="?<?= http_build_query(array_merge($_GET, ['page' => 1])) ?>">1</a>
<?php if ($start > 2): ?><span>...</span><?php endif; ?>
<?php endif;
for ($i = $start; $i <= $end; $i++): ?>
<?php if ($i == $page): ?>
<span class="current"><?= $i ?></span>
<?php else: ?>
<a href="?<?= http_build_query(array_merge($_GET, ['page' => $i])) ?>"><?= $i ?></a>
<?php endif; ?>
<?php endfor;
if ($end < $total_pages): ?>
<?php if ($end < $total_pages - 1): ?><span>...</span><?php endif; ?>
<a href="?<?= http_build_query(array_merge($_GET, ['page' => $total_pages])) ?>"><?= $total_pages ?></a>
<?php endif; ?>
<?php if ($page < $total_pages): ?>
<a href="?<?= http_build_query(array_merge($_GET, ['page' => $page + 1])) ?>">
<i class="fas fa-chevron-right"></i>
</a>
<?php endif; ?>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
</div>
<script>
function shareCrate(crateId, crateName) {
const url = window.location.origin + '/crate/' + crateId;
if (navigator.share) {
navigator.share({
title: crateName,
url: url
});
} else if (navigator.clipboard) {
navigator.clipboard.writeText(url).then(() => {
if (typeof showNotification === 'function') {
showNotification('<?= t('crates.link_copied') ?>', 'success');
} else {
alert('<?= t('crates.link_copied') ?>');
}
});
}
}
// Play crate from card
async function playCrateFromCard(crateId, button) {
// Find all buttons for this crate (overlay and action button)
const card = button.closest('.crate-card');
const allButtons = card.querySelectorAll('.crate-play-btn-large, .crate-btn.play-btn');
// Set loading state
allButtons.forEach(btn => {
btn.classList.add('loading');
const icon = btn.querySelector('i');
if (icon) {
icon.className = 'fas fa-spinner fa-spin';
}
btn.disabled = true;
});
try {
// Fetch crate tracks
const response = await fetch(`/api/get_crate_tracks.php?crate_id=${crateId}&public=1`);
const data = await response.json();
if (!data.success || !data.tracks || data.tracks.length === 0) {
throw new Error(data.error || 'No tracks found in this crate');
}
// Format tracks for playlist (we'll fetch signed URLs when playing)
const formattedTracks = data.tracks.map(track => ({
id: track.id,
title: track.title || 'Untitled',
artist_name: track.artist_name || data.crate.artist_name || 'Unknown Artist',
duration: track.duration || 300,
user_id: track.user_id || null,
artist_id: track.user_id || null // Support both for compatibility
}));
// Load playlist into global player
if (window.enhancedGlobalPlayer && typeof window.enhancedGlobalPlayer.loadPagePlaylist === 'function') {
window.enhancedGlobalPlayer.loadPagePlaylist(formattedTracks, 'crate', 0);
} else {
// Fallback
window._communityPlaylist = formattedTracks;
window._communityPlaylistType = 'crate';
window._communityTrackIndex = 0;
}
// Play first track
if (formattedTracks.length > 0) {
const firstTrack = formattedTracks[0];
// Fetch signed audio token for first track
const tokenResponse = await fetch(`/api/get_audio_token.php?track_id=${firstTrack.id}&duration=${firstTrack.duration}`);
const tokenData = await tokenResponse.json();
if (tokenData.success && tokenData.url) {
// Play using global player
if (window.enhancedGlobalPlayer && typeof window.enhancedGlobalPlayer.playTrack === 'function') {
window.enhancedGlobalPlayer.playTrack(
tokenData.url,
firstTrack.title,
firstTrack.artist_name,
firstTrack.id,
firstTrack.user_id || firstTrack.artist_id
);
} else if (typeof window.playTrack === 'function') {
window.playTrack(
tokenData.url,
firstTrack.title,
firstTrack.artist_name,
firstTrack.id,
firstTrack.user_id || firstTrack.artist_id
);
}
// Show notification
if (typeof window.showNotification === 'function') {
window.showNotification(`🎵 Playing: ${data.crate.name} (${formattedTracks.length} tracks)`, 'success');
}
} else {
throw new Error('Failed to get audio token');
}
}
} catch (error) {
console.error('Error playing crate:', error);
if (typeof window.showNotification === 'function') {
window.showNotification('Error loading crate: ' + error.message, 'error');
} else {
alert('Error loading crate: ' + error.message);
}
} finally {
// Reset button states
allButtons.forEach(btn => {
btn.classList.remove('loading');
const icon = btn.querySelector('i');
if (icon) {
icon.className = 'fas fa-play';
}
btn.disabled = false;
});
}
}
// Open crate modal
async function openCrateModal(crateId) {
const modal = document.getElementById('crateViewModal');
const modalContent = document.getElementById('crateModalContent');
const modalTitle = document.getElementById('crateModalTitle');
if (!modal || !modalContent) return;
// Show loading state
modalContent.innerHTML = `
<div style="text-align: center; padding: 3rem;">
<i class="fas fa-spinner fa-spin" style="font-size: 2rem; color: #667eea; margin-bottom: 1rem;"></i>
<p style="color: #a0aec0;"><?= t('crates.loading') ?></p>
</div>
`;
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
try {
// Fetch crate details
const response = await fetch(`/api/get_crate_tracks.php?crate_id=${crateId}&public=1`);
const data = await response.json();
if (!data.success || !data.crate) {
throw new Error(data.error || 'Failed to load crate');
}
const crate = data.crate;
const tracks = data.tracks || [];
// Format duration
const totalSeconds = tracks.reduce((sum, track) => sum + (parseInt(track.duration) || 0), 0);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const durationFormatted = hours > 0
? `${hours}h ${minutes}m`
: `${minutes}m`;
// Build modal content
const artistAvatar = crate.profile_image ? `<img src="${escapeHtml(crate.profile_image)}" alt="${escapeHtml(crate.artist_name)}" class="crate-modal-artist-avatar" onerror="this.style.display='none'">` : '';
let tracksHtml = '';
if (tracks.length > 0) {
tracksHtml = tracks.map((track, index) => `
<div class="crate-modal-track" data-track-id="${track.id}">
<div class="crate-modal-track-number">${index + 1}</div>
<div class="crate-modal-track-info">
<div class="crate-modal-track-title">${escapeHtml(track.title || 'Untitled')}</div>
<div class="crate-modal-track-artist">${escapeHtml(track.artist_name || crate.artist_name || 'Unknown')}</div>
</div>
<div class="crate-modal-track-duration">${formatTrackDuration(track.duration || 0)}</div>
</div>
`).join('');
} else {
tracksHtml = '<div style="text-align: center; padding: 2rem; color: #a0aec0;"><?= t("crates.no_tracks") ?></div>';
}
modalTitle.textContent = escapeHtml(crate.name);
modalContent.innerHTML = `
<div class="crate-modal-header">
<div class="crate-modal-artwork">
<i class="fas fa-box-open"></i>
</div>
<div class="crate-modal-info">
<div class="crate-modal-type"><?= t('crates.crate') ?></div>
<h2 class="crate-modal-name">${escapeHtml(crate.name)}</h2>
<div class="crate-modal-artist">
${artistAvatar}
<span><?= t('crates.by') ?> <a href="/artist/${crate.artist_id}" target="_blank">${escapeHtml(crate.artist_name)}</a></span>
</div>
<div class="crate-modal-stats">
<div class="crate-modal-stat">
<i class="fas fa-music"></i>
<span>${tracks.length} <?= t('crates.tracks') ?></span>
</div>
<div class="crate-modal-stat">
<i class="fas fa-clock"></i>
<span>${durationFormatted}</span>
</div>
</div>
${crate.description && crate.is_description_public ? `<div class="crate-modal-description">${escapeHtml(crate.description)}</div>` : ''}
<div class="crate-modal-actions">
<button class="crate-modal-btn primary" onclick="playCrateFromModal(${crateId})">
<i class="fas fa-play"></i>
<span><?= t('library.crates.play_all') ?></span>
</button>
<a href="/crate/${crateId}" class="crate-modal-btn secondary" target="_blank">
<i class="fas fa-external-link-alt"></i>
<span><?= t('crates.view_full') ?></span>
</a>
<button class="crate-modal-btn secondary" onclick="shareCrate(${crateId}, '${escapeHtml(crate.name).replace(/'/g, "\\'")}')">
<i class="fas fa-share-alt"></i>
</button>
</div>
</div>
</div>
<div class="crate-modal-tracks">
<h3 class="crate-modal-tracks-title"><?= t('crates.track_list') ?></h3>
<div class="crate-modal-tracks-list">
${tracksHtml}
</div>
</div>
`;
} catch (error) {
console.error('Error loading crate:', error);
modalContent.innerHTML = `
<div style="text-align: center; padding: 3rem;">
<i class="fas fa-exclamation-triangle" style="font-size: 2rem; color: #e53e3e; margin-bottom: 1rem;"></i>
<p style="color: #a0aec0;">${escapeHtml(error.message || '<?= t("crates.load_error") ?>')}</p>
</div>
`;
}
}
// Play crate from modal
async function playCrateFromModal(crateId) {
await playCrateFromCard(crateId, document.querySelector(`[data-crate-id="${crateId}"] .crate-btn.play-btn`));
}
// Close modal
function closeCrateModal() {
const modal = document.getElementById('crateViewModal');
if (modal) {
modal.style.display = 'none';
document.body.style.overflow = '';
}
}
// Escape HTML helper
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Format track duration
function formatTrackDuration(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
// Close modal on overlay click
document.addEventListener('click', function(e) {
const modal = document.getElementById('crateViewModal');
if (modal && e.target === modal) {
closeCrateModal();
}
});
// Close modal on escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeCrateModal();
}
});
// Improve search form UX
document.addEventListener('DOMContentLoaded', function() {
const searchForm = document.getElementById('cratesSearchForm');
const searchInput = document.getElementById('cratesSearchInput');
const searchSubmitBtn = document.querySelector('.search-submit-btn');
if (searchForm && searchInput) {
// Allow Enter key to submit
searchInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
searchForm.submit();
}
});
// Show loading state on submit
searchForm.addEventListener('submit', function() {
if (searchSubmitBtn) {
searchSubmitBtn.disabled = true;
const originalHTML = searchSubmitBtn.innerHTML;
searchSubmitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
// Re-enable after a delay in case of error
setTimeout(() => {
searchSubmitBtn.disabled = false;
searchSubmitBtn.innerHTML = originalHTML;
}, 3000);
}
});
}
});
</script>
<!-- Crate View Modal -->
<div id="crateViewModal" class="crate-view-modal" style="display: none;">
<div class="crate-view-modal-content">
<button class="crate-view-modal-close" onclick="closeCrateModal()">
<i class="fas fa-times"></i>
</button>
<h2 id="crateModalTitle" class="crate-view-modal-title"></h2>
<div id="crateModalContent" class="crate-view-modal-body"></div>
</div>
</div>
<style>
/* Crate View Modal Styles */
.crate-view-modal {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(10px);
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
overflow-y: auto;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.crate-view-modal-content {
background: linear-gradient(135deg, rgba(26, 26, 46, 0.95) 0%, rgba(22, 33, 62, 0.95) 100%);
border: 1px solid rgba(102, 126, 234, 0.3);
border-radius: 24px;
max-width: 900px;
width: 100%;
max-height: 90vh;
overflow-y: auto;
position: relative;
animation: slideUp 0.3s ease;
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
}
@keyframes slideUp {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.crate-view-modal-close {
position: absolute;
top: 1.5rem;
right: 1.5rem;
width: 40px;
height: 40px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: white;
font-size: 1.2rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
z-index: 10;
}
.crate-view-modal-close:hover {
background: rgba(255, 255, 255, 0.2);
transform: rotate(90deg);
}
.crate-view-modal-title {
font-size: 1.5rem;
font-weight: 700;
color: white;
padding: 2rem 2rem 1rem;
margin: 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.crate-view-modal-body {
padding: 2rem;
}
.crate-modal-header {
display: flex;
gap: 2rem;
margin-bottom: 2rem;
padding-bottom: 2rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.crate-modal-artwork {
width: 150px;
height: 150px;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.4) 0%, rgba(118, 75, 162, 0.4) 100%);
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.crate-modal-artwork i {
font-size: 4rem;
color: white;
}
.crate-modal-info {
flex: 1;
}
.crate-modal-type {
font-size: 0.85rem;
color: #667eea;
text-transform: uppercase;
letter-spacing: 1px;
font-weight: 600;
margin-bottom: 0.5rem;
}
.crate-modal-name {
font-size: 2rem;
font-weight: 700;
color: white;
margin: 0 0 1rem 0;
}
.crate-modal-artist {
display: flex;
align-items: center;
gap: 0.75rem;
color: #a0aec0;
font-size: 1rem;
margin-bottom: 1rem;
}
.crate-modal-artist-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
object-fit: cover;
border: 2px solid rgba(102, 126, 234, 0.3);
}
.crate-modal-artist a {
color: #667eea;
text-decoration: none;
}
.crate-modal-artist a:hover {
text-decoration: underline;
}
.crate-modal-stats {
display: flex;
gap: 2rem;
margin-bottom: 1rem;
}
.crate-modal-stat {
display: flex;
align-items: center;
gap: 0.5rem;
color: #a0aec0;
font-size: 0.95rem;
}
.crate-modal-stat i {
color: #667eea;
}
.crate-modal-description {
color: #a0aec0;
font-size: 0.95rem;
line-height: 1.6;
margin-bottom: 1.5rem;
}
.crate-modal-actions {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.crate-modal-btn {
padding: 0.75rem 1.5rem;
border-radius: 10px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
transition: all 0.2s ease;
text-decoration: none;
border: none;
}
.crate-modal-btn.primary {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
}
.crate-modal-btn.primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
}
.crate-modal-btn.secondary {
background: rgba(255, 255, 255, 0.08);
color: white;
border: 1px solid rgba(255, 255, 255, 0.15);
}
.crate-modal-btn.secondary:hover {
background: rgba(255, 255, 255, 0.12);
}
.crate-modal-tracks {
margin-top: 2rem;
}
.crate-modal-tracks-title {
font-size: 1.25rem;
font-weight: 600;
color: white;
margin-bottom: 1rem;
}
.crate-modal-tracks-list {
max-height: 400px;
overflow-y: auto;
border-radius: 12px;
background: rgba(255, 255, 255, 0.03);
padding: 0.5rem;
}
.crate-modal-track {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.75rem;
border-radius: 8px;
transition: background 0.2s ease;
cursor: pointer;
}
.crate-modal-track:hover {
background: rgba(255, 255, 255, 0.05);
}
.crate-modal-track-number {
width: 30px;
text-align: center;
color: #6b7280;
font-size: 0.9rem;
font-weight: 600;
}
.crate-modal-track-info {
flex: 1;
min-width: 0;
}
.crate-modal-track-title {
color: white;
font-size: 0.95rem;
font-weight: 500;
margin-bottom: 0.25rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.crate-modal-track-artist {
color: #a0aec0;
font-size: 0.85rem;
}
.crate-modal-track-duration {
color: #6b7280;
font-size: 0.9rem;
font-weight: 500;
}
@media (max-width: 768px) {
.crate-view-modal {
padding: 1rem;
}
.crate-view-modal-content {
max-height: 95vh;
}
.crate-modal-header {
flex-direction: column;
}
.crate-modal-artwork {
width: 120px;
height: 120px;
margin: 0 auto;
}
.crate-modal-name {
font-size: 1.5rem;
text-align: center;
}
.crate-modal-actions {
flex-direction: column;
}
.crate-modal-btn {
width: 100%;
justify-content: center;
}
}
</style>
<?php include 'includes/footer.php'; ?>