Initial dev export (exclude uploads/runtime)

This commit is contained in:
AudioCore Bot
2026-03-04 20:46:11 +00:00
commit b2afadd539
120 changed files with 20410 additions and 0 deletions

View File

@@ -0,0 +1,168 @@
<?php
$pageTitle = $title ?? 'Edit Release';
$release = $release ?? [];
$error = $error ?? '';
$uploadError = (string)($_GET['upload_error'] ?? '');
$storePluginEnabled = (bool)($store_plugin_enabled ?? false);
ob_start();
?>
<section class="admin-card">
<div class="badge">Releases</div>
<div style="display:flex; align-items:center; justify-content:space-between; gap:16px; margin-top:16px;">
<div>
<h1 style="font-size:28px; margin:0;"><?= htmlspecialchars($pageTitle, ENT_QUOTES, 'UTF-8') ?></h1>
<p style="color: var(--muted); margin-top:6px;">Create or update a release.</p>
</div>
<div style="display:flex; gap:10px; align-items:center;">
<?php if ((int)($release['id'] ?? 0) > 0): ?>
<a href="/admin/releases/tracks?release_id=<?= (int)$release['id'] ?>" class="btn outline">Manage Tracks</a>
<?php endif; ?>
<a href="/admin/releases" class="btn outline">Back</a>
</div>
</div>
<?php if ($error): ?>
<div style="margin-top:16px; color:#f3b0b0; font-size:13px;"><?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?></div>
<?php endif; ?>
<?php if ($uploadError !== ''): ?>
<div style="margin-top:12px; color:#f3b0b0; font-size:13px;"><?= htmlspecialchars($uploadError, ENT_QUOTES, 'UTF-8') ?></div>
<?php endif; ?>
<form method="post" action="/admin/releases/save" enctype="multipart/form-data" style="margin-top:16px; display:grid; gap:16px;">
<input type="hidden" name="id" value="<?= (int)($release['id'] ?? 0) ?>">
<div class="admin-card" style="padding:16px;">
<div style="display:grid; gap:12px;">
<label class="label">Title</label>
<input class="input" name="title" value="<?= htmlspecialchars((string)($release['title'] ?? ''), ENT_QUOTES, 'UTF-8') ?>" placeholder="Release title">
<label class="label">Artist</label>
<input class="input" name="artist_name" value="<?= htmlspecialchars((string)($release['artist_name'] ?? ''), ENT_QUOTES, 'UTF-8') ?>" placeholder="Artist name">
<label class="label">Slug</label>
<input class="input" name="slug" value="<?= htmlspecialchars((string)($release['slug'] ?? ''), ENT_QUOTES, 'UTF-8') ?>" placeholder="release-title">
<label class="label">Release Date</label>
<input class="input" type="date" name="release_date" value="<?= htmlspecialchars((string)($release['release_date'] ?? ''), ENT_QUOTES, 'UTF-8') ?>">
<label class="label">Catalog Number</label>
<input class="input" name="catalog_no" value="<?= htmlspecialchars((string)($release['catalog_no'] ?? ''), ENT_QUOTES, 'UTF-8') ?>" placeholder="CAT-001">
<div class="admin-card" style="padding:14px; background: rgba(10,10,12,0.6);">
<div class="label">Upload cover</div>
<input type="hidden" name="release_id" value="<?= (int)($release['id'] ?? 0) ?>">
<label for="releaseCoverFile" id="releaseCoverDropzone" style="display:flex; flex-direction:column; gap:8px; align-items:center; justify-content:center; padding:18px; border-radius:14px; border:1px dashed rgba(255,255,255,0.2); background: rgba(0,0,0,0.2); cursor:pointer;">
<div style="font-size:11px; text-transform:uppercase; letter-spacing:0.2em; color:var(--muted);">Drag &amp; Drop</div>
<div style="font-size:13px; color:var(--text);">or click to upload</div>
<div id="releaseCoverFileName" style="font-size:11px; color:var(--muted);">No file selected</div>
</label>
<input class="input" type="file" id="releaseCoverFile" name="release_cover" accept="image/*" style="display:none;">
<div style="margin-top:10px; display:flex; justify-content:flex-end;">
<button type="submit" class="btn small" formaction="/admin/releases/upload" formmethod="post" formenctype="multipart/form-data" name="upload_type" value="cover">Upload</button>
</div>
</div>
<div style="display:flex; align-items:center; justify-content:space-between; gap:12px;">
<label class="label" style="margin:0;">Cover URL</label>
<button type="button" class="btn outline small" data-media-picker="release_cover_url" data-media-picker-mode="url">Pick from Media</button>
</div>
<input class="input" id="release_cover_url" name="cover_url" value="<?= htmlspecialchars((string)($release['cover_url'] ?? ''), ENT_QUOTES, 'UTF-8') ?>" placeholder="https://...">
<div class="admin-card" style="padding:14px; background: rgba(10,10,12,0.6);">
<div class="label">Upload sample (MP3)</div>
<input type="hidden" name="release_id" value="<?= (int)($release['id'] ?? 0) ?>">
<label for="releaseSampleFile" id="releaseSampleDropzone" style="display:flex; flex-direction:column; gap:8px; align-items:center; justify-content:center; padding:18px; border-radius:14px; border:1px dashed rgba(255,255,255,0.2); background: rgba(0,0,0,0.2); cursor:pointer;">
<div style="font-size:11px; text-transform:uppercase; letter-spacing:0.2em; color:var(--muted);">Drag &amp; Drop</div>
<div style="font-size:13px; color:var(--text);">or click to upload</div>
<div id="releaseSampleFileName" style="font-size:11px; color:var(--muted);">No file selected</div>
</label>
<input class="input" type="file" id="releaseSampleFile" name="release_sample" accept="audio/mpeg" style="display:none;">
<div style="margin-top:10px; display:flex; justify-content:flex-end;">
<button type="submit" class="btn small" formaction="/admin/releases/upload" formmethod="post" formenctype="multipart/form-data" name="upload_type" value="sample">Upload</button>
</div>
</div>
<label class="label">Sample URL (MP3)</label>
<input class="input" name="sample_url" value="<?= htmlspecialchars((string)($release['sample_url'] ?? ''), ENT_QUOTES, 'UTF-8') ?>" placeholder="https://...">
<label class="label">Description</label>
<textarea class="input" name="description" rows="6" style="resize:vertical; font-family:'IBM Plex Mono', monospace; font-size:13px; line-height:1.6;"><?= htmlspecialchars((string)($release['description'] ?? ''), ENT_QUOTES, 'UTF-8') ?></textarea>
<label class="label">Release Credits</label>
<textarea class="input" name="credits" rows="4" style="resize:vertical; font-family:'IBM Plex Mono', monospace; font-size:13px; line-height:1.6;" placeholder="Written by..., Produced by..., Mastered by..."><?= htmlspecialchars((string)($release['credits'] ?? ''), ENT_QUOTES, 'UTF-8') ?></textarea>
<?php if ($storePluginEnabled): ?>
<div class="admin-card" style="padding:14px; background: rgba(10,10,12,0.6);">
<div class="label" style="margin-bottom:10px;">Store Options</div>
<label style="display:flex; align-items:center; gap:8px; font-size:12px; color:var(--muted); text-transform:uppercase; letter-spacing:0.2em;">
<input type="checkbox" name="store_enabled" value="1" <?= ((int)($release['store_enabled'] ?? 0) === 1) ? 'checked' : '' ?>>
Enable release purchase
</label>
<div style="display:grid; grid-template-columns:1fr 120px; gap:10px; margin-top:10px;">
<div>
<label class="label">Bundle Price</label>
<input class="input" name="bundle_price" value="<?= htmlspecialchars((string)($release['bundle_price'] ?? ''), ENT_QUOTES, 'UTF-8') ?>" placeholder="3.99">
</div>
<div>
<label class="label">Currency</label>
<input class="input" name="store_currency" value="<?= htmlspecialchars((string)($release['store_currency'] ?? 'GBP'), ENT_QUOTES, 'UTF-8') ?>" placeholder="GBP">
</div>
</div>
<label class="label" style="margin-top:10px;">Button Label (optional)</label>
<input class="input" name="purchase_label" value="<?= htmlspecialchars((string)($release['purchase_label'] ?? ''), ENT_QUOTES, 'UTF-8') ?>" placeholder="Buy Release">
</div>
<?php endif; ?>
<label style="display:flex; align-items:center; gap:8px; font-size:12px; color:var(--muted); text-transform:uppercase; letter-spacing:0.2em;">
<input type="checkbox" name="is_published" value="1" <?= ((int)($release['is_published'] ?? 1) === 1) ? 'checked' : '' ?>>
Published
</label>
</div>
</div>
<div style="display:flex; justify-content:flex-end; gap:12px; align-items:center;">
<button type="submit" class="btn">Save release</button>
</div>
</form>
</section>
<script>
(function () {
const coverDrop = document.getElementById('releaseCoverDropzone');
const coverFile = document.getElementById('releaseCoverFile');
const coverName = document.getElementById('releaseCoverFileName');
if (coverDrop && coverFile && coverName) {
coverDrop.addEventListener('dragover', (event) => {
event.preventDefault();
coverDrop.style.borderColor = 'var(--accent)';
});
coverDrop.addEventListener('dragleave', () => {
coverDrop.style.borderColor = 'rgba(255,255,255,0.2)';
});
coverDrop.addEventListener('drop', (event) => {
event.preventDefault();
coverDrop.style.borderColor = 'rgba(255,255,255,0.2)';
if (event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files.length) {
coverFile.files = event.dataTransfer.files;
coverName.textContent = event.dataTransfer.files[0].name;
}
});
coverFile.addEventListener('change', () => {
coverName.textContent = coverFile.files.length ? coverFile.files[0].name : 'No file selected';
});
}
const sampleDrop = document.getElementById('releaseSampleDropzone');
const sampleFile = document.getElementById('releaseSampleFile');
const sampleName = document.getElementById('releaseSampleFileName');
if (sampleDrop && sampleFile && sampleName) {
sampleDrop.addEventListener('dragover', (event) => {
event.preventDefault();
sampleDrop.style.borderColor = 'var(--accent)';
});
sampleDrop.addEventListener('dragleave', () => {
sampleDrop.style.borderColor = 'rgba(255,255,255,0.2)';
});
sampleDrop.addEventListener('drop', (event) => {
event.preventDefault();
sampleDrop.style.borderColor = 'rgba(255,255,255,0.2)';
if (event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files.length) {
sampleFile.files = event.dataTransfer.files;
sampleName.textContent = event.dataTransfer.files[0].name;
}
});
sampleFile.addEventListener('change', () => {
sampleName.textContent = sampleFile.files.length ? sampleFile.files[0].name : 'No file selected';
});
}
})();
</script>
<?php
$content = ob_get_clean();
require __DIR__ . '/../../../../modules/admin/views/layout.php';

View File

@@ -0,0 +1,87 @@
<?php
$pageTitle = 'Releases';
$releases = $releases ?? [];
$tableReady = $table_ready ?? false;
$pageId = (int)($page_id ?? 0);
$pagePublished = (int)($page_published ?? 0);
ob_start();
?>
<section class="admin-card">
<div class="badge">Releases</div>
<div style="display:flex; align-items:center; justify-content:space-between; gap:16px; margin-top:16px;">
<div>
<h1 style="font-size:28px; margin:0;">Releases</h1>
<p style="color: var(--muted); margin-top:6px;">Manage singles, EPs, and albums.</p>
</div>
<a href="/admin/releases/new" class="btn">New Release</a>
</div>
<?php if (!$tableReady): ?>
<div class="admin-card" style="margin-top:16px; padding:16px; display:flex; align-items:center; justify-content:space-between; gap:16px;">
<div>
<div style="font-weight:600;">Database not initialized</div>
<div style="color: var(--muted); font-size:13px; margin-top:4px;">Create the releases table before adding records.</div>
</div>
<form method="post" action="/admin/releases/install">
<button type="submit" class="btn small">Create Tables</button>
</form>
</div>
<?php else: ?>
<div class="admin-card" style="margin-top:16px; padding:14px; display:flex; align-items:center; justify-content:space-between; gap:16px;">
<div>
<div style="font-weight:600;">Releases page</div>
<div style="color: var(--muted); font-size:12px; margin-top:4px;">
Slug: <code>releases</code>
<?php if ($pageId > 0): ?>
- Status: <?= $pagePublished === 1 ? 'Published' : 'Draft' ?>
<?php else: ?>
- Not created
<?php endif; ?>
</div>
</div>
<?php if ($pageId > 0): ?>
<a href="/admin/pages/edit?id=<?= $pageId ?>" class="btn outline small">Edit Page Content</a>
<?php else: ?>
<span class="pill">Re-enable plugin to create</span>
<?php endif; ?>
</div>
<?php if (!$releases): ?>
<div style="margin-top:18px; color: var(--muted); font-size:13px;">No releases yet.</div>
<?php else: ?>
<div style="margin-top:18px; display:grid; gap:12px;">
<?php foreach ($releases as $release): ?>
<div class="admin-card" style="padding:14px; display:flex; align-items:center; justify-content:space-between; gap:16px;">
<div style="display:flex; gap:12px; align-items:center;">
<div style="width:44px; height:44px; border-radius:12px; overflow:hidden; background:rgba(255,255,255,0.06); display:grid; place-items:center;">
<?php if (!empty($release['cover_url'])): ?>
<img src="<?= htmlspecialchars((string)$release['cover_url'], ENT_QUOTES, 'UTF-8') ?>" alt="" style="width:100%; height:100%; object-fit:cover;">
<?php else: ?>
<span style="font-size:12px; color:var(--muted);">N/A</span>
<?php endif; ?>
</div>
<div>
<div style="font-weight:600;"><?= htmlspecialchars((string)$release['title'], ENT_QUOTES, 'UTF-8') ?></div>
<div style="font-size:12px; color:var(--muted);"><?= htmlspecialchars((string)$release['slug'], ENT_QUOTES, 'UTF-8') ?></div>
</div>
</div>
<div style="display:flex; gap:8px; align-items:center;">
<?php if ((int)$release['is_published'] !== 1): ?>
<span class="pill">Draft</span>
<?php endif; ?>
<a href="/admin/releases/tracks?release_id=<?= (int)$release['id'] ?>" class="btn outline small">Tracks</a>
<a href="/admin/releases/edit?id=<?= (int)$release['id'] ?>" class="btn outline small">Edit</a>
<form method="post" action="/admin/releases/delete" onsubmit="return confirm('Delete this release?');">
<input type="hidden" name="id" value="<?= (int)$release['id'] ?>">
<button type="submit" class="btn outline small">Delete</button>
</form>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php endif; ?>
</section>
<?php
$content = ob_get_clean();
require __DIR__ . '/../../../../modules/admin/views/layout.php';

View File

@@ -0,0 +1,161 @@
<?php
$pageTitle = $title ?? 'Edit Track';
$track = $track ?? [];
$release = $release ?? null;
$error = $error ?? '';
$uploadError = (string)($_GET['upload_error'] ?? '');
$storePluginEnabled = (bool)($store_plugin_enabled ?? false);
ob_start();
?>
<section class="admin-card">
<div class="badge">Releases</div>
<div style="display:flex; align-items:center; justify-content:space-between; gap:16px; margin-top:16px;">
<div>
<h1 style="font-size:28px; margin:0;"><?= htmlspecialchars($pageTitle, ENT_QUOTES, 'UTF-8') ?></h1>
<p style="color: var(--muted); margin-top:6px;">
<?= $release ? htmlspecialchars((string)$release['title'], ENT_QUOTES, 'UTF-8') : 'Track details' ?>
</p>
</div>
<a href="/admin/releases/tracks?release_id=<?= (int)($track['release_id'] ?? 0) ?>" class="btn outline">Back</a>
</div>
<?php if ($error): ?>
<div style="margin-top:16px; color:#f3b0b0; font-size:13px;"><?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?></div>
<?php endif; ?>
<?php if ($uploadError !== ''): ?>
<div style="margin-top:12px; color:#f3b0b0; font-size:13px;"><?= htmlspecialchars($uploadError, ENT_QUOTES, 'UTF-8') ?></div>
<?php endif; ?>
<form method="post" action="/admin/releases/tracks/save" enctype="multipart/form-data" style="margin-top:16px; display:grid; gap:16px;">
<input type="hidden" name="id" value="<?= (int)($track['id'] ?? 0) ?>">
<input type="hidden" name="track_id" value="<?= (int)($track['id'] ?? 0) ?>">
<input type="hidden" name="release_id" value="<?= (int)($track['release_id'] ?? 0) ?>">
<div class="admin-card" style="padding:16px;">
<div style="display:grid; gap:12px;">
<label class="label">Track #</label>
<input class="input" name="track_no" value="<?= htmlspecialchars((string)($track['track_no'] ?? ''), ENT_QUOTES, 'UTF-8') ?>" placeholder="1">
<label class="label">Title</label>
<input class="input" name="title" value="<?= htmlspecialchars((string)($track['title'] ?? ''), ENT_QUOTES, 'UTF-8') ?>" placeholder="Track title">
<label class="label">Mix Name</label>
<input class="input" name="mix_name" value="<?= htmlspecialchars((string)($track['mix_name'] ?? ''), ENT_QUOTES, 'UTF-8') ?>" placeholder="Extended Mix">
<label class="label">Duration</label>
<input class="input" name="duration" value="<?= htmlspecialchars((string)($track['duration'] ?? ''), ENT_QUOTES, 'UTF-8') ?>" placeholder="6:12">
<label class="label">BPM</label>
<input class="input" name="bpm" value="<?= htmlspecialchars((string)($track['bpm'] ?? ''), ENT_QUOTES, 'UTF-8') ?>" placeholder="140">
<label class="label">Key</label>
<input class="input" name="key_signature" value="<?= htmlspecialchars((string)($track['key_signature'] ?? ''), ENT_QUOTES, 'UTF-8') ?>" placeholder="B Minor">
<div class="admin-card" style="padding:14px; background: rgba(10,10,12,0.6);">
<div class="label">Upload sample (MP3)</div>
<label for="trackSampleFile" id="trackSampleDropzone" style="display:flex; flex-direction:column; gap:8px; align-items:center; justify-content:center; padding:18px; border-radius:14px; border:1px dashed rgba(255,255,255,0.2); background: rgba(0,0,0,0.2); cursor:pointer;">
<div style="font-size:11px; text-transform:uppercase; letter-spacing:0.2em; color:var(--muted);">Drag &amp; Drop</div>
<div style="font-size:13px; color:var(--text);">or click to upload</div>
<div id="trackSampleFileName" style="font-size:11px; color:var(--muted);">No file selected</div>
</label>
<input class="input" type="file" id="trackSampleFile" name="track_sample" accept="audio/mpeg" style="display:none;">
<div style="margin-top:10px; display:flex; justify-content:flex-end;">
<button type="submit" class="btn small" formaction="/admin/releases/tracks/upload" formmethod="post" formenctype="multipart/form-data" name="upload_kind" value="sample">Upload</button>
</div>
</div>
<label class="label">Sample URL (MP3)</label>
<input class="input" name="sample_url" value="<?= htmlspecialchars((string)($track['sample_url'] ?? ''), ENT_QUOTES, 'UTF-8') ?>" placeholder="https://...">
<?php if ($storePluginEnabled): ?>
<div class="admin-card" style="padding:14px; background: rgba(10,10,12,0.6);">
<div class="label">Upload full track (MP3)</div>
<label for="trackFullFile" id="trackFullDropzone" style="display:flex; flex-direction:column; gap:8px; align-items:center; justify-content:center; padding:18px; border-radius:14px; border:1px dashed rgba(255,255,255,0.2); background: rgba(0,0,0,0.2); cursor:pointer;">
<div style="font-size:11px; text-transform:uppercase; letter-spacing:0.2em; color:var(--muted);">Drag &amp; Drop</div>
<div style="font-size:13px; color:var(--text);">or click to upload</div>
<div id="trackFullFileName" style="font-size:11px; color:var(--muted);">No file selected</div>
</label>
<input class="input" type="file" id="trackFullFile" name="track_sample" accept="audio/mpeg" style="display:none;">
<div style="margin-top:10px; display:flex; justify-content:flex-end;">
<button type="submit" class="btn small" formaction="/admin/releases/tracks/upload" formmethod="post" formenctype="multipart/form-data" name="upload_kind" value="full">Upload Full</button>
</div>
</div>
<label class="label">Full File URL (Store Download)</label>
<input class="input" name="full_file_url" value="<?= htmlspecialchars((string)($track['full_file_url'] ?? ''), ENT_QUOTES, 'UTF-8') ?>" placeholder="/uploads/media/track-full.mp3">
<div class="admin-card" style="padding:14px; background: rgba(10,10,12,0.6);">
<div class="label" style="margin-bottom:10px;">Store Options</div>
<label style="display:flex; align-items:center; gap:8px; font-size:12px; color:var(--muted); text-transform:uppercase; letter-spacing:0.2em;">
<input type="checkbox" name="store_enabled" value="1" <?= ((int)($track['store_enabled'] ?? 0) === 1) ? 'checked' : '' ?>>
Enable track purchase
</label>
<div style="display:grid; grid-template-columns:1fr 120px; gap:10px; margin-top:10px;">
<div>
<label class="label">Track Price</label>
<input class="input" name="track_price" value="<?= htmlspecialchars((string)($track['track_price'] ?? ''), ENT_QUOTES, 'UTF-8') ?>" placeholder="1.49">
</div>
<div>
<label class="label">Currency</label>
<input class="input" name="store_currency" value="<?= htmlspecialchars((string)($track['store_currency'] ?? 'GBP'), ENT_QUOTES, 'UTF-8') ?>" placeholder="GBP">
</div>
</div>
</div>
<?php endif; ?>
</div>
</div>
<div style="display:flex; justify-content:flex-end; gap:12px; align-items:center;">
<button type="submit" class="btn">Save track</button>
</div>
</form>
</section>
<script>
(function () {
const drop = document.getElementById('trackSampleDropzone');
const file = document.getElementById('trackSampleFile');
const name = document.getElementById('trackSampleFileName');
if (drop && file && name) {
drop.addEventListener('dragover', (event) => {
event.preventDefault();
drop.style.borderColor = 'var(--accent)';
});
drop.addEventListener('dragleave', () => {
drop.style.borderColor = 'rgba(255,255,255,0.2)';
});
drop.addEventListener('drop', (event) => {
event.preventDefault();
drop.style.borderColor = 'rgba(255,255,255,0.2)';
if (event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files.length) {
file.files = event.dataTransfer.files;
name.textContent = event.dataTransfer.files[0].name;
}
});
file.addEventListener('change', () => {
name.textContent = file.files.length ? file.files[0].name : 'No file selected';
});
}
const fullDrop = document.getElementById('trackFullDropzone');
const fullFile = document.getElementById('trackFullFile');
const fullName = document.getElementById('trackFullFileName');
if (fullDrop && fullFile && fullName) {
fullDrop.addEventListener('dragover', (event) => {
event.preventDefault();
fullDrop.style.borderColor = 'var(--accent)';
});
fullDrop.addEventListener('dragleave', () => {
fullDrop.style.borderColor = 'rgba(255,255,255,0.2)';
});
fullDrop.addEventListener('drop', (event) => {
event.preventDefault();
fullDrop.style.borderColor = 'rgba(255,255,255,0.2)';
if (event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files.length) {
fullFile.files = event.dataTransfer.files;
fullName.textContent = event.dataTransfer.files[0].name;
}
});
fullFile.addEventListener('change', () => {
fullName.textContent = fullFile.files.length ? fullFile.files[0].name : 'No file selected';
});
}
})();
</script>
<?php
$content = ob_get_clean();
require __DIR__ . '/../../../../modules/admin/views/layout.php';

View File

@@ -0,0 +1,69 @@
<?php
$pageTitle = 'Release Tracks';
$release = $release ?? null;
$tracks = $tracks ?? [];
$tableReady = $table_ready ?? false;
$releaseId = (int)($release_id ?? 0);
ob_start();
?>
<section class="admin-card">
<div class="badge">Releases</div>
<div style="display:flex; align-items:center; justify-content:space-between; gap:16px; margin-top:16px;">
<div>
<h1 style="font-size:28px; margin:0;">Tracks</h1>
<p style="color: var(--muted); margin-top:6px;">
<?= $release ? htmlspecialchars((string)$release['title'], ENT_QUOTES, 'UTF-8') : 'Select a release to manage tracks.' ?>
</p>
</div>
<div style="display:flex; gap:10px; align-items:center;">
<a href="/admin/releases" class="btn outline">Back</a>
<?php if ($releaseId > 0): ?>
<a href="/admin/releases/tracks/new?release_id=<?= $releaseId ?>" class="btn">New Track</a>
<?php endif; ?>
</div>
</div>
<?php if (!$tableReady): ?>
<div style="margin-top:18px; color: var(--muted); font-size:13px;">Tracks table is not available. Run Releases ? Create Tables.</div>
<?php elseif (!$release): ?>
<div style="margin-top:18px; color: var(--muted); font-size:13px;">Release not found.</div>
<?php elseif (!$tracks): ?>
<div style="margin-top:18px; color: var(--muted); font-size:13px;">No tracks yet.</div>
<?php else: ?>
<div style="margin-top:18px; display:grid; gap:12px;">
<?php foreach ($tracks as $track): ?>
<div class="admin-card" style="padding:14px; display:flex; align-items:center; justify-content:space-between; gap:16px;">
<div style="display:flex; gap:12px; align-items:center;">
<div style="width:38px; height:38px; border-radius:10px; background:rgba(255,255,255,0.06); display:grid; place-items:center; font-size:12px; color:var(--muted);">
<?= (int)($track['track_no'] ?? 0) > 0 ? (int)$track['track_no'] : '<27>' ?>
</div>
<div>
<div style="font-weight:600;">
<?= htmlspecialchars((string)$track['title'], ENT_QUOTES, 'UTF-8') ?>
<?php if (!empty($track['mix_name'])): ?>
<span style="color:var(--muted); font-weight:400;">(<?= htmlspecialchars((string)$track['mix_name'], ENT_QUOTES, 'UTF-8') ?>)</span>
<?php endif; ?>
</div>
<div style="font-size:12px; color:var(--muted);">
<?= htmlspecialchars((string)($track['duration'] ?? ''), ENT_QUOTES, 'UTF-8') ?>
<?= !empty($track['bpm']) ? ' <20> ' . htmlspecialchars((string)$track['bpm'], ENT_QUOTES, 'UTF-8') . ' BPM' : '' ?>
<?= !empty($track['key_signature']) ? ' <20> ' . htmlspecialchars((string)$track['key_signature'], ENT_QUOTES, 'UTF-8') : '' ?>
</div>
</div>
</div>
<div style="display:flex; gap:8px; align-items:center;">
<a href="/admin/releases/tracks/edit?release_id=<?= $releaseId ?>&id=<?= (int)$track['id'] ?>" class="btn outline small">Edit</a>
<form method="post" action="/admin/releases/tracks/delete" onsubmit="return confirm('Delete this track?');">
<input type="hidden" name="id" value="<?= (int)$track['id'] ?>">
<input type="hidden" name="release_id" value="<?= $releaseId ?>">
<button type="submit" class="btn outline small">Delete</button>
</form>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</section>
<?php
$content = ob_get_clean();
require __DIR__ . '/../../../../modules/admin/views/layout.php';

View File

@@ -0,0 +1,366 @@
<?php
$pageTitle = $title ?? 'Releases';
$releases = is_array($releases ?? null) ? $releases : [];
$releaseCount = (int)($total_releases ?? count($releases));
$artistFilter = trim((string)($artist_filter ?? ''));
$artistOptions = is_array($artist_options ?? null) ? $artist_options : [];
$search = trim((string)($search ?? ''));
$sort = trim((string)($sort ?? 'newest'));
$currentPage = max(1, (int)($current_page ?? 1));
$totalPages = max(1, (int)($total_pages ?? 1));
$buildReleaseUrl = static function (int $page) use ($search, $artistFilter, $sort): string {
$params = [];
if ($search !== '') {
$params['q'] = $search;
}
if ($artistFilter !== '') {
$params['artist'] = $artistFilter;
}
if ($sort !== 'newest') {
$params['sort'] = $sort;
}
if ($page > 1) {
$params['p'] = $page;
}
$qs = http_build_query($params);
return '/releases' . ($qs !== '' ? ('?' . $qs) : '');
};
ob_start();
?>
<div class="ac-releases-page">
<section class="card ac-releases-shell">
<div class="ac-releases-header">
<div class="badge">Releases</div>
<h1>Latest Drops</h1>
<p>Singles, EPs, and albums from the AudioCore catalog.</p>
</div>
<form method="get" action="/releases" class="ac-release-controls">
<div class="ac-search-wrap">
<span class="ac-search-icon"><i class="fa-solid fa-magnifying-glass"></i></span>
<input class="ac-search-input" type="text" name="q" value="<?= htmlspecialchars($search, ENT_QUOTES, 'UTF-8') ?>" placeholder="Search releases, artists, catalog number">
</div>
<select class="ac-control-input" name="artist">
<option value="">All artists</option>
<?php foreach ($artistOptions as $artist): ?>
<option value="<?= htmlspecialchars((string)$artist, ENT_QUOTES, 'UTF-8') ?>" <?= $artistFilter === (string)$artist ? 'selected' : '' ?>>
<?= htmlspecialchars((string)$artist, ENT_QUOTES, 'UTF-8') ?>
</option>
<?php endforeach; ?>
</select>
<select class="ac-control-input" name="sort">
<option value="newest" <?= $sort === 'newest' ? 'selected' : '' ?>>Newest first</option>
<option value="oldest" <?= $sort === 'oldest' ? 'selected' : '' ?>>Oldest first</option>
<option value="title_asc" <?= $sort === 'title_asc' ? 'selected' : '' ?>>Title A-Z</option>
<option value="title_desc" <?= $sort === 'title_desc' ? 'selected' : '' ?>>Title Z-A</option>
</select>
<button type="submit" class="ac-btn ac-btn-primary">Apply</button>
<a href="/releases" class="ac-btn ac-btn-ghost">Reset</a>
</form>
<?php if ($artistFilter !== '' || $search !== '' || $sort !== 'newest'): ?>
<div class="ac-active-filters">
<?php if ($artistFilter !== ''): ?><div class="ac-chip">Artist: <?= htmlspecialchars($artistFilter, ENT_QUOTES, 'UTF-8') ?></div><?php endif; ?>
<?php if ($search !== ''): ?><div class="ac-chip">Search: <?= htmlspecialchars($search, ENT_QUOTES, 'UTF-8') ?></div><?php endif; ?>
<?php if ($sort !== 'newest'): ?><div class="ac-chip">Sort: <?= htmlspecialchars(str_replace('_', ' ', $sort), ENT_QUOTES, 'UTF-8') ?></div><?php endif; ?>
</div>
<?php endif; ?>
<?php if (!$releases): ?>
<div class="ac-empty">No releases published yet.</div>
<?php else: ?>
<div class="ac-release-grid">
<?php foreach ($releases as $release): ?>
<a class="ac-release-card" href="/release?slug=<?= htmlspecialchars((string)$release['slug'], ENT_QUOTES, 'UTF-8') ?>">
<div class="ac-release-cover">
<?php if (!empty($release['cover_url'])): ?>
<img src="<?= htmlspecialchars((string)$release['cover_url'], ENT_QUOTES, 'UTF-8') ?>" alt="">
<?php else: ?>
<div class="ac-release-placeholder">AC</div>
<?php endif; ?>
<?php if (!empty($release['release_date'])): ?>
<span class="ac-release-date"><?= htmlspecialchars((string)$release['release_date'], ENT_QUOTES, 'UTF-8') ?></span>
<?php endif; ?>
</div>
<div class="ac-release-meta">
<div class="ac-release-title"><?= htmlspecialchars((string)$release['title'], ENT_QUOTES, 'UTF-8') ?></div>
<?php if (!empty($release['artist_name'])): ?>
<div class="ac-release-artist"><?= htmlspecialchars((string)$release['artist_name'], ENT_QUOTES, 'UTF-8') ?></div>
<?php endif; ?>
</div>
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if ($totalPages > 1): ?>
<nav class="ac-release-pagination">
<?php $prevPage = max(1, $currentPage - 1); ?>
<?php $nextPage = min($totalPages, $currentPage + 1); ?>
<a class="ac-btn ac-btn-ghost<?= $currentPage <= 1 ? ' is-disabled' : '' ?>" href="<?= $currentPage <= 1 ? '#' : htmlspecialchars($buildReleaseUrl($prevPage), ENT_QUOTES, 'UTF-8') ?>">Prev</a>
<div class="ac-pagination-meta">Page <?= $currentPage ?> of <?= $totalPages ?> · <?= $releaseCount ?> total</div>
<a class="ac-btn ac-btn-ghost<?= $currentPage >= $totalPages ? ' is-disabled' : '' ?>" href="<?= $currentPage >= $totalPages ? '#' : htmlspecialchars($buildReleaseUrl($nextPage), ENT_QUOTES, 'UTF-8') ?>">Next</a>
</nav>
<?php endif; ?>
</section>
</div>
<style>
.ac-releases-page .ac-releases-shell {
margin-top: 14px;
display: grid;
gap: 14px;
}
.ac-releases-page .ac-releases-header {
border-bottom: 1px solid rgba(255,255,255,0.06);
padding-bottom: 10px;
}
.ac-releases-page .ac-releases-header h1 {
margin: 8px 0 0;
font-size: 52px;
line-height: 1.05;
letter-spacing: -0.02em;
}
.ac-releases-page .ac-releases-header p {
margin: 8px 0 0;
color: var(--muted);
font-size: 14px;
}
.ac-releases-page .ac-release-controls {
display: grid;
grid-template-columns: minmax(260px, 1fr) 180px 180px auto auto;
gap: 10px;
align-items: center;
padding: 8px;
border: 1px solid rgba(255,255,255,0.07);
border-radius: 14px;
background: rgba(8, 12, 19, 0.16);
}
.ac-releases-page .ac-search-wrap {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
border: 1px solid rgba(255,255,255,0.07);
border-radius: 10px;
background: rgba(7,10,16,0.10);
height: 36px;
padding: 0 10px;
}
.ac-releases-page .ac-search-icon {
color: rgba(255,255,255,0.72);
font-size: 12px;
width: 16px;
display: inline-flex;
justify-content: center;
}
.ac-releases-page .ac-search-input,
.ac-releases-page .ac-control-input {
width: 100%;
height: 36px;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.07);
background: rgba(7,10,16,0.10);
color: rgba(233,237,247,0.94);
padding: 0 10px;
font-size: 13px;
outline: none;
}
.ac-releases-page .ac-search-input {
border: 0;
background: transparent;
padding: 0;
min-width: 0;
}
.ac-releases-page .ac-search-input::placeholder {
color: rgba(220,228,245,.45);
}
.ac-releases-page .ac-search-input:focus,
.ac-releases-page .ac-control-input:focus {
box-shadow: 0 0 0 2px rgba(34,242,165,.12);
border-color: rgba(34,242,165,.38);
}
.ac-releases-page .ac-btn {
display: inline-flex;
align-items: center;
justify-content: center;
height: 36px;
padding: 0 14px;
border-radius: 10px;
border: 1px solid rgba(255,255,255,.10);
text-decoration: none;
font-size: 10px;
text-transform: uppercase;
letter-spacing: .12em;
font-weight: 700;
cursor: pointer;
transition: all .2s ease;
}
.ac-releases-page .ac-btn-primary {
background: rgba(34,242,165,.14);
color: #87f2cb;
border-color: rgba(34,242,165,.34);
}
.ac-releases-page .ac-btn-primary:hover {
background: rgba(34,242,165,.20);
}
.ac-releases-page .ac-btn-ghost {
color: #a1acc4;
background: transparent;
border-color: rgba(255,255,255,.10);
}
.ac-releases-page .ac-btn-ghost:hover {
color: #e5ebf7;
border-color: rgba(255,255,255,.18);
}
.ac-releases-page .ac-btn.is-disabled {
opacity: .45;
pointer-events: none;
}
.ac-releases-page .ac-active-filters {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.ac-releases-page .ac-chip {
border-radius: 999px;
padding: 6px 10px;
border: 1px solid rgba(255,255,255,.12);
font-size: 10px;
letter-spacing: .12em;
text-transform: uppercase;
color: var(--muted);
font-family: 'IBM Plex Mono', monospace;
}
.ac-releases-page .ac-release-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 240px));
justify-content: start;
gap: 16px;
}
.ac-releases-page .ac-release-card {
display: grid;
gap: 10px;
color: inherit;
text-decoration: none;
padding: 10px;
border-radius: 18px;
border: 1px solid rgba(255,255,255,.08);
background: rgba(0,0,0,.14);
transition: border-color .2s ease, transform .2s ease;
}
.ac-releases-page .ac-release-card:hover {
border-color: rgba(255,255,255,.18);
transform: translateY(-2px);
}
.ac-releases-page .ac-release-cover {
width: 100%;
aspect-ratio: 1 / 1;
border-radius: 16px;
overflow: hidden;
position: relative;
background: rgba(255,255,255,.03);
}
.ac-releases-page .ac-release-cover img {
width: 100%;
height: 100%;
object-fit: cover;
}
.ac-releases-page .ac-release-date {
position: absolute;
left: 10px;
bottom: 10px;
border-radius: 999px;
padding: 6px 10px;
font-size: 11px;
color: #fff;
background: rgba(0,0,0,.56);
border: 1px solid rgba(255,255,255,.18);
}
.ac-releases-page .ac-release-placeholder {
width: 100%;
height: 100%;
display: grid;
place-items: center;
color: var(--muted);
letter-spacing: .16em;
}
.ac-releases-page .ac-release-meta {
display: grid;
gap: 6px;
}
.ac-releases-page .ac-release-title {
font-weight: 600;
font-size: 18px;
line-height: 1.2;
}
.ac-releases-page .ac-release-artist {
font-size: 13px;
color: var(--muted);
}
.ac-releases-page .ac-empty {
padding: 16px;
border: 1px solid rgba(255,255,255,.08);
border-radius: 12px;
color: var(--muted);
}
.ac-releases-page .ac-release-pagination {
margin-top: 2px;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
flex-wrap: wrap;
}
.ac-releases-page .ac-pagination-meta {
font-size: 12px;
color: var(--muted);
}
@media (max-width: 1180px) {
.ac-releases-page .ac-release-controls {
grid-template-columns: 1fr 160px 160px auto auto;
}
.ac-releases-page .ac-release-grid {
grid-template-columns: repeat(auto-fill, minmax(210px, 230px));
}
}
@media (max-width: 900px) {
.ac-releases-page .ac-release-controls {
grid-template-columns: 1fr 1fr 1fr;
}
.ac-releases-page .ac-release-controls .ac-search-wrap {
grid-column: 1 / -1;
}
}
@media (max-width: 760px) {
.ac-releases-page .ac-releases-header h1 {
font-size: 40px;
}
.ac-releases-page .ac-release-controls {
grid-template-columns: 1fr;
}
.ac-releases-page .ac-release-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
justify-content: stretch;
}
}
@media (max-width: 520px) {
.ac-releases-page .ac-release-grid {
grid-template-columns: 1fr;
}
}
</style>
<?php
$content = ob_get_clean();
require __DIR__ . '/../../../../views/site/layout.php';

View File

@@ -0,0 +1,326 @@
<?php
declare(strict_types=1);
$pageTitle = $title ?? 'Release';
$release = $release ?? null;
$tracks = $tracks ?? [];
$storePluginEnabled = (bool)($store_plugin_enabled ?? false);
$releaseCover = (string)($release['cover_url'] ?? '');
$returnUrl = (string)($_SERVER['REQUEST_URI'] ?? '/releases');
$releaseStoreEnabled = (int)($release['store_enabled'] ?? 0) === 1;
$bundlePrice = (float)($release['bundle_price'] ?? 0);
$bundleCurrency = (string)($release['store_currency'] ?? 'GBP');
$bundleLabel = trim((string)($release['purchase_label'] ?? ''));
$bundleLabel = $bundleLabel !== '' ? $bundleLabel : 'Buy Release';
ob_start();
?>
<section class="card" style="display:grid; gap:18px; padding-bottom:110px;">
<div class="badge">Release</div>
<?php if (!$release): ?>
<h1 style="margin:0; font-size:28px;">Release not found</h1>
<p style="color:var(--muted);">This release is unavailable.</p>
<?php else: ?>
<div class="release-wrap" style="display:grid; gap:18px;">
<div class="release-hero" style="display:grid; grid-template-columns:minmax(0,1fr) 360px; gap:22px; align-items:start;">
<div class="release-meta" style="display:grid; gap:14px;">
<h1 style="margin:0; font-size:46px; line-height:1.06;"><?= htmlspecialchars((string)$release['title'], ENT_QUOTES, 'UTF-8') ?></h1>
<?php if (!empty($release['artist_name'])): ?>
<div style="font-size:14px; color:var(--muted);">
By
<a href="/releases?artist=<?= rawurlencode((string)$release['artist_name']) ?>" style="color:#dfe7fb; text-decoration:none; border-bottom:1px solid rgba(223,231,251,.35);">
<?= htmlspecialchars((string)$release['artist_name'], ENT_QUOTES, 'UTF-8') ?>
</a>
</div>
<?php endif; ?>
<div style="display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); gap:10px; max-width:640px;">
<?php if (!empty($release['catalog_no'])): ?>
<div style="padding:10px 12px; border-radius:12px; border:1px solid rgba(255,255,255,0.08); background:rgba(0,0,0,0.22);">
<div class="badge" style="font-size:9px;">Catalog</div>
<div style="margin-top:6px; font-size:14px;"><?= htmlspecialchars((string)$release['catalog_no'], ENT_QUOTES, 'UTF-8') ?></div>
</div>
<?php endif; ?>
<?php if (!empty($release['release_date'])): ?>
<div style="padding:10px 12px; border-radius:12px; border:1px solid rgba(255,255,255,0.08); background:rgba(0,0,0,0.22);">
<div class="badge" style="font-size:9px;">Release Date</div>
<div style="margin-top:6px; font-size:14px;"><?= htmlspecialchars((string)$release['release_date'], ENT_QUOTES, 'UTF-8') ?></div>
</div>
<?php endif; ?>
</div>
<?php if (!empty($release['description'])): ?>
<div style="color:var(--muted); line-height:1.75; max-width:760px;">
<?= nl2br(htmlspecialchars((string)$release['description'], ENT_QUOTES, 'UTF-8')) ?>
</div>
<?php endif; ?>
<?php if ($storePluginEnabled && $releaseStoreEnabled && $bundlePrice > 0): ?>
<div style="margin-top:4px;">
<form method="post" action="/store/cart/add" style="margin:0;">
<input type="hidden" name="item_type" value="release">
<input type="hidden" name="item_id" value="<?= (int)($release['id'] ?? 0) ?>">
<input type="hidden" name="title" value="<?= htmlspecialchars((string)$release['title'], ENT_QUOTES, 'UTF-8') ?>">
<input type="hidden" name="cover_url" value="<?= htmlspecialchars($releaseCover, ENT_QUOTES, 'UTF-8') ?>">
<input type="hidden" name="currency" value="<?= htmlspecialchars($bundleCurrency, ENT_QUOTES, 'UTF-8') ?>">
<input type="hidden" name="price" value="<?= htmlspecialchars(number_format($bundlePrice, 2, '.', ''), ENT_QUOTES, 'UTF-8') ?>">
<input type="hidden" name="qty" value="1">
<input type="hidden" name="return_url" value="<?= htmlspecialchars($returnUrl, ENT_QUOTES, 'UTF-8') ?>">
<button type="submit" class="track-buy-btn">
<i class="fa-solid fa-cart-plus"></i>
<span><?= htmlspecialchars($bundleLabel, ENT_QUOTES, 'UTF-8') ?> <?= htmlspecialchars($bundleCurrency, ENT_QUOTES, 'UTF-8') ?> <?= number_format($bundlePrice, 2) ?></span>
</button>
</form>
</div>
<?php endif; ?>
</div>
<div class="release-cover-box" style="border-radius:20px; overflow:hidden; background:rgba(255,255,255,0.05); border:1px solid rgba(255,255,255,0.1); aspect-ratio:1/1;">
<?php if ($releaseCover !== ''): ?>
<img id="releaseCoverMain" src="<?= htmlspecialchars($releaseCover, ENT_QUOTES, 'UTF-8') ?>" alt="" style="width:100%; height:100%; object-fit:cover;">
<?php else: ?>
<div id="releaseCoverMain" style="height:100%; display:grid; place-items:center; color:var(--muted); letter-spacing:0.3em; font-size:12px;">AUDIOCORE</div>
<?php endif; ?>
</div>
</div>
<?php if ($tracks): ?>
<div style="padding:16px; border-radius:18px; border:1px solid rgba(255,255,255,0.08); background:rgba(0,0,0,0.22); display:grid; gap:10px;">
<div style="display:flex; align-items:center; justify-content:space-between; gap:12px;">
<div class="badge">Tracklist</div>
<div style="display:flex; align-items:center; gap:10px;">
<div id="releasePlayerNow" style="font-size:12px; color:var(--muted);">Select a track to play sample</div>
</div>
</div>
<div style="display:grid; gap:8px;">
<?php foreach ($tracks as $track): ?>
<?php
$sample = (string)($track['sample_url'] ?? '');
$trackTitle = (string)($track['title'] ?? 'Track');
$mix = (string)($track['mix_name'] ?? '');
$fullTitle = $mix !== '' ? ($trackTitle . ' (' . $mix . ')') : $trackTitle;
?>
<div class="track-row" style="display:grid; grid-template-columns:92px minmax(0,1fr) auto; gap:12px; align-items:center; padding:10px 12px; border-radius:12px; border:1px solid rgba(255,255,255,0.08); background:rgba(0,0,0,0.26);">
<button type="button"
class="track-play-btn"
data-src="<?= htmlspecialchars($sample, ENT_QUOTES, 'UTF-8') ?>"
data-title="<?= htmlspecialchars($fullTitle, ENT_QUOTES, 'UTF-8') ?>"
data-cover="<?= htmlspecialchars($releaseCover, ENT_QUOTES, 'UTF-8') ?>"
<?= $sample === '' ? 'disabled' : '' ?>>
<i class="fa-solid fa-play"></i> <span>Play</span>
</button>
<div style="min-width:0;">
<div style="font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">
<?= htmlspecialchars($trackTitle, ENT_QUOTES, 'UTF-8') ?>
<?php if ($mix !== ''): ?>
<span style="color:var(--muted); font-weight:400;">(<?= htmlspecialchars($mix, ENT_QUOTES, 'UTF-8') ?>)</span>
<?php endif; ?>
</div>
<div style="font-size:12px; color:var(--muted); margin-top:4px;">
<?= (int)($track['track_no'] ?? 0) > 0 ? '#' . (int)$track['track_no'] : 'Track' ?>
<?= !empty($track['duration']) ? ' - ' . htmlspecialchars((string)$track['duration'], ENT_QUOTES, 'UTF-8') : '' ?>
<?= !empty($track['bpm']) ? ' - ' . htmlspecialchars((string)$track['bpm'], ENT_QUOTES, 'UTF-8') . ' BPM' : '' ?>
<?= !empty($track['key_signature']) ? ' - ' . htmlspecialchars((string)$track['key_signature'], ENT_QUOTES, 'UTF-8') : '' ?>
</div>
</div>
<div style="display:flex; align-items:center; gap:8px;">
<?php
$trackStoreEnabled = (int)($track['store_enabled'] ?? 0) === 1;
$trackPrice = (float)($track['track_price'] ?? 0);
$trackCurrency = (string)($track['store_currency'] ?? 'GBP');
?>
<?php if ($storePluginEnabled && $trackStoreEnabled && $trackPrice > 0): ?>
<form method="post" action="/store/cart/add" style="margin:0;">
<input type="hidden" name="item_type" value="track">
<input type="hidden" name="item_id" value="<?= (int)($track['id'] ?? 0) ?>">
<input type="hidden" name="title" value="<?= htmlspecialchars($fullTitle, ENT_QUOTES, 'UTF-8') ?>">
<input type="hidden" name="cover_url" value="<?= htmlspecialchars($releaseCover, ENT_QUOTES, 'UTF-8') ?>">
<input type="hidden" name="currency" value="<?= htmlspecialchars($trackCurrency, ENT_QUOTES, 'UTF-8') ?>">
<input type="hidden" name="price" value="<?= htmlspecialchars(number_format($trackPrice, 2, '.', ''), ENT_QUOTES, 'UTF-8') ?>">
<input type="hidden" name="qty" value="1">
<input type="hidden" name="return_url" value="<?= htmlspecialchars($returnUrl, ENT_QUOTES, 'UTF-8') ?>">
<button type="submit" class="track-buy-btn">
<i class="fa-solid fa-cart-plus"></i>
<span>Buy <?= htmlspecialchars($trackCurrency, ENT_QUOTES, 'UTF-8') ?> <?= number_format($trackPrice, 2) ?></span>
</button>
</form>
<?php else: ?>
<div style="font-size:11px; color:var(--muted); text-transform:uppercase; letter-spacing:0.18em;">
<?= $sample !== '' ? 'Sample' : 'No sample' ?>
</div>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<?php if (!empty($release['credits'])): ?>
<div style="padding:12px 14px; border-radius:14px; border:1px solid rgba(255,255,255,0.08); background:rgba(0,0,0,0.2);">
<div class="badge" style="font-size:9px;">Credits</div>
<div style="margin-top:6px; color:var(--muted); line-height:1.55; font-size:13px;">
<?= nl2br(htmlspecialchars((string)$release['credits'], ENT_QUOTES, 'UTF-8')) ?>
</div>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
</section>
<div id="acDock" class="ac-dock" hidden>
<div class="ac-dock-inner">
<div class="ac-dock-meta">
<div id="acDockArt" class="ac-dock-art">AC</div>
<div class="ac-dock-title-wrap">
<div class="badge" style="font-size:9px;">Now Playing</div>
<div id="acDockTitle" class="ac-dock-title">Track sample</div>
</div>
</div>
<button type="button" id="acDockToggle" class="ac-dock-toggle"><i class="fa-solid fa-play"></i> <span>Play</span></button>
<input id="acDockSeek" class="ac-seek" type="range" min="0" max="100" value="0" step="0.1">
<div class="ac-dock-time"><span id="acDockCurrent">0:00</span> / <span id="acDockDuration">0:00</span></div>
<input id="acDockVolume" class="ac-volume" type="range" min="0" max="1" value="1" step="0.01">
<button type="button" id="acDockClose" class="ac-dock-close" aria-label="Close player">X</button>
<audio id="acDockAudio" preload="none"></audio>
</div>
</div>
<style>
.release-wrap {
min-width: 0;
}
.track-row.is-active { border-color: rgba(34,242,165,.45)!important; background: rgba(34,242,165,.08)!important; }
.track-play-btn,.ac-dock-toggle{height:34px;border:1px solid rgba(34,242,165,.35);border-radius:999px;background:rgba(34,242,165,.12);color:#bffff0;font-weight:700;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:0 14px}
.track-buy-btn{height:34px;border:1px solid rgba(255,255,255,.18);border-radius:999px;background:rgba(255,255,255,.08);color:#e9eefc;font-weight:700;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:0 14px;font-size:12px;white-space:nowrap}
.track-buy-btn:hover{background:rgba(255,255,255,.14)}
.track-play-btn[disabled]{border-color:rgba(255,255,255,.2);background:rgba(255,255,255,.08);color:var(--muted);cursor:not-allowed}
.ac-dock{position:fixed;left:14px;right:14px;bottom:14px;z-index:50}
.ac-dock-inner{display:grid;grid-template-columns:minmax(210px,280px) 96px minmax(160px,1fr) 110px 110px 44px;gap:10px;align-items:center;padding:12px;border-radius:14px;border:1px solid rgba(255,255,255,.14);background:rgba(10,11,15,.95)}
.ac-dock-meta{display:grid;grid-template-columns:44px minmax(0,1fr);align-items:center;gap:10px;min-width:0}
.ac-dock-art{width:44px;height:44px;border-radius:10px;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.06);overflow:hidden;display:grid;place-items:center;font-size:10px;color:var(--muted)}
.ac-dock-art img{width:100%;height:100%;object-fit:cover;display:block}
.ac-dock-title-wrap{min-width:0}.ac-dock-title{margin-top:4px;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ac-dock-time{font-size:12px;color:var(--muted);font-family:'IBM Plex Mono',monospace}
.ac-dock-close{height:34px;border-radius:10px;border:1px solid rgba(255,255,255,.2);background:rgba(255,255,255,.05);color:var(--muted);cursor:pointer}
.ac-seek,.ac-volume{-webkit-appearance:none;appearance:none;height:6px;border-radius:999px;background:rgba(255,255,255,.2);outline:none}
.ac-seek::-webkit-slider-thumb,.ac-volume::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:14px;height:14px;border-radius:50%;background:#22f2a5;border:1px solid rgba(0,0,0,.45)}
@media (max-width: 980px) {
.release-hero {
grid-template-columns: 1fr !important;
gap: 16px !important;
}
.release-cover-box {
max-width: 420px;
}
}
@media (max-width: 700px) {
.release-meta h1 {
font-size: 32px !important;
line-height: 1.12 !important;
}
.track-row {
grid-template-columns: 88px minmax(0,1fr) !important;
gap: 10px !important;
}
.track-buy-btn { font-size: 11px; padding: 0 10px; }
.ac-dock {
left: 8px;
right: 8px;
bottom: 8px;
}
.ac-dock-inner {
grid-template-columns: minmax(0,1fr) 96px auto 44px !important;
grid-template-areas:
"meta toggle time close"
"seek seek seek seek";
row-gap: 8px;
padding: 10px;
border-radius: 12px;
}
.ac-dock-meta { grid-area: meta; gap: 8px; }
.ac-dock-art { width: 38px; height: 38px; border-radius: 8px; }
.ac-dock-title { font-size: 12px; }
.ac-dock-close { grid-area: close; width: 44px; justify-self: end; }
.ac-dock-toggle {
grid-area: toggle;
height: 32px;
padding: 0 8px;
border-radius: 999px;
justify-self: center;
width: 96px;
min-width: 96px;
font-size: 12px;
}
.ac-dock-time {
grid-area: time;
text-align: right;
white-space: nowrap;
font-size: 12px;
align-self: center;
}
.ac-seek { grid-area: seek; margin-top: 0; align-self: center; }
.ac-volume { display: none; }
}
</style>
<script>
(function () {
const dock = document.getElementById('acDock');
const audio = document.getElementById('acDockAudio');
const toggle = document.getElementById('acDockToggle');
const seek = document.getElementById('acDockSeek');
const volume = document.getElementById('acDockVolume');
const current = document.getElementById('acDockCurrent');
const duration = document.getElementById('acDockDuration');
const titleEl = document.getElementById('acDockTitle');
const dockArt = document.getElementById('acDockArt');
const closeBtn = document.getElementById('acDockClose');
const status = document.getElementById('releasePlayerNow');
const mainCover = document.getElementById('releaseCoverMain');
if (!dock || !audio || !toggle || !seek || !volume || !current || !duration || !titleEl || !closeBtn) return;
const mainCoverSrc = mainCover && mainCover.tagName === 'IMG' ? (mainCover.getAttribute('src') || '') : '';
const defaultCover = mainCoverSrc || <?= json_encode($releaseCover, JSON_UNESCAPED_SLASHES) ?> || '';
let activeBtn = null;
function fmt(sec){ if(!isFinite(sec)||sec<0) return '0:00'; const m=Math.floor(sec/60), s=Math.floor(sec%60); return m+':'+String(s).padStart(2,'0'); }
function setPlayState(btn,playing){ if(!btn) return; btn.innerHTML = playing ? '<i class="fa-solid fa-pause"></i> <span>Pause</span>' : '<i class="fa-solid fa-play"></i> <span>Play</span>'; }
function setDockArt(src){ if(!dockArt) return; const finalSrc = src || defaultCover; dockArt.innerHTML = finalSrc ? ('<img src="'+finalSrc.replace(/"/g,'&quot;')+'" alt="">') : 'AC'; }
function setActive(btn,title){
document.querySelectorAll('.track-play-btn').forEach((b)=>{ setPlayState(b,false); const row=b.closest('.track-row'); if(row) row.classList.remove('is-active'); });
activeBtn = btn;
if(btn){ setPlayState(btn,true); const row=btn.closest('.track-row'); if(row) row.classList.add('is-active'); }
titleEl.textContent = title || 'Track sample';
if(status) status.textContent = title ? ('Now Playing: '+title) : 'Select a track to play sample';
}
function openAndPlay(src,title,btn,cover){
if(!src) return;
if(dock.hidden) dock.hidden = false;
setDockArt(cover || '');
if(audio.getAttribute('src') === src){ if(audio.paused) audio.play().catch(()=>{}); else audio.pause(); return; }
audio.setAttribute('src', src);
setActive(btn,title);
audio.play().catch(()=>{ if(btn) setPlayState(btn,false); });
}
document.querySelectorAll('.track-play-btn').forEach((btn)=>{
btn.addEventListener('click', ()=>{
openAndPlay(btn.getAttribute('data-src') || '', btn.getAttribute('data-title') || 'Track sample', btn, btn.getAttribute('data-cover') || '');
});
});
toggle.addEventListener('click', ()=>{ if(!audio.getAttribute('src')) return; if(audio.paused) audio.play().catch(()=>{}); else audio.pause(); });
closeBtn.addEventListener('click', ()=>{ audio.pause(); audio.removeAttribute('src'); seek.value='0'; current.textContent='0:00'; duration.textContent='0:00'; setPlayState(toggle,false); setActive(null,''); setDockArt(''); dock.hidden=true; });
volume.addEventListener('input', ()=>{ audio.volume = Number(volume.value); });
seek.addEventListener('input', ()=>{ if(!isFinite(audio.duration)||audio.duration<=0) return; audio.currentTime = (Number(seek.value)/100)*audio.duration; });
audio.addEventListener('loadedmetadata', ()=>{ duration.textContent = fmt(audio.duration); });
audio.addEventListener('timeupdate', ()=>{ current.textContent = fmt(audio.currentTime); if(isFinite(audio.duration)&&audio.duration>0){ seek.value = String((audio.currentTime/audio.duration)*100); } });
audio.addEventListener('play', ()=>{ setPlayState(toggle,true); if(activeBtn) setPlayState(activeBtn,true); });
audio.addEventListener('pause', ()=>{ setPlayState(toggle,false); if(activeBtn) setPlayState(activeBtn,false); });
audio.addEventListener('ended', ()=>{ setPlayState(toggle,false); if(activeBtn) setPlayState(activeBtn,false); seek.value='0'; current.textContent='0:00'; });
})();
</script>
<?php
$content = ob_get_clean();
require __DIR__ . '/../../../../views/site/layout.php';