![]() 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/.cursor-server/data/User/History/6b08286c/ |
<?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;
}
.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;
}
/* 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(320px, 1fr));
gap: 1.5rem;
}
/* Crate Card */
.crate-card {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
overflow: hidden;
transition: all 0.3s ease;
}
.crate-card:hover {
transform: translateY(-4px);
border-color: rgba(102, 126, 234, 0.4);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
}
.crate-card-header {
background: linear-gradient(135deg, rgba(102, 126, 234, 0.2) 0%, rgba(118, 75, 162, 0.2) 100%);
padding: 1.25rem;
display: flex;
align-items: center;
gap: 1rem;
}
.crate-icon {
width: 56px;
height: 56px;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
color: white;
flex-shrink: 0;
}
.crate-title-info {
flex: 1;
min-width: 0;
}
.crate-title {
font-size: 1.15rem;
font-weight: 600;
color: white;
margin-bottom: 0.25rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.crate-title a {
color: inherit;
text-decoration: none;
transition: color 0.2s;
}
.crate-title a:hover {
color: #667eea;
}
.crate-artist {
font-size: 0.9rem;
color: #a0aec0;
}
.crate-artist a {
color: #a0aec0;
text-decoration: none;
transition: color 0.2s;
}
.crate-artist a:hover {
color: #667eea;
}
.crate-card-body {
padding: 1.25rem;
}
.crate-description {
color: #a0aec0;
font-size: 0.9rem;
line-height: 1.5;
margin-bottom: 1rem;
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: 1rem;
}
.crate-stat {
display: flex;
align-items: center;
gap: 0.5rem;
color: #6b7280;
font-size: 0.9rem;
}
.crate-stat i {
color: #667eea;
}
.crate-actions {
display: flex;
gap: 0.75rem;
}
.crate-btn {
flex: 1;
padding: 0.75rem 1rem;
border: none;
border-radius: 8px;
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;
}
.crate-btn.primary {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
}
.crate-btn.primary:hover {
transform: scale(1.02);
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
}
.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);
}
/* 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-header h1 {
font-size: 2rem;
}
.filters-row {
flex-direction: column;
}
.search-box, .filter-select {
width: 100%;
}
.crates-grid {
grid-template-columns: 1fr;
}
.stats-bar {
flex-direction: column;
gap: 0.5rem;
text-align: center;
}
}
</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="">
<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') ?>">
</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" style="flex: 0;">
<i class="fas fa-search"></i> <?= t('crates.search_btn') ?>
</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']);
?>
<div class="crate-card">
<div class="crate-card-header">
<div class="crate-icon">
<i class="fas fa-box-open"></i>
</div>
<div class="crate-title-info">
<div class="crate-title">
<a href="/crate/<?= $crate['id'] ?>"><?= htmlspecialchars($crate['name']) ?></a>
</div>
<div class="crate-artist">
<?= t('crates.by') ?> <a href="/artist/<?= $crate['artist_id'] ?>" target="_blank"><?= htmlspecialchars($crate['artist_name']) ?></a>
</div>
</div>
</div>
<div class="crate-card-body">
<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">
<a href="/crate/<?= $crate['id'] ?>" class="crate-btn primary">
<i class="fas fa-eye"></i> <?= t('crates.view') ?>
</a>
<button class="crate-btn secondary" onclick="shareCrate(<?= $crate['id'] ?>, '<?= htmlspecialchars($crate['name'], ENT_QUOTES) ?>')">
<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') ?>');
}
});
}
}
</script>
<?php include 'includes/footer.php'; ?>