Redirects Manager
diff --git a/modules/pages/module.php b/modules/pages/module.php
index 75490f3..c063941 100644
--- a/modules/pages/module.php
+++ b/modules/pages/module.php
@@ -2,10 +2,37 @@
declare(strict_types=1);
use Core\Http\Router;
+use Core\Services\Shortcodes;
use Modules\Pages\PagesController;
require_once __DIR__ . '/PagesController.php';
+Shortcodes::register('hero', static function (array $attrs = []): string {
+ $eyebrow = trim((string)($attrs['eyebrow'] ?? 'Catalog Focus'));
+ $title = trim((string)($attrs['title'] ?? 'Latest Drops'));
+ $subtitle = trim((string)($attrs['subtitle'] ?? 'Discover fresh releases, artists, and top-selling tracks.'));
+ $ctaText = trim((string)($attrs['cta_text'] ?? 'Browse Releases'));
+ $ctaUrl = trim((string)($attrs['cta_url'] ?? '/releases'));
+ $secondaryText = trim((string)($attrs['secondary_text'] ?? 'Meet the Roster'));
+ $secondaryUrl = trim((string)($attrs['secondary_url'] ?? '/artists'));
+
+ $html = '
';
+ if ($eyebrow !== '') {
+ $html .= '' . htmlspecialchars($eyebrow, ENT_QUOTES, 'UTF-8') . '
';
+ }
+ $html .= '' . htmlspecialchars($title, ENT_QUOTES, 'UTF-8') . '
';
+ if ($subtitle !== '') {
+ $html .= '' . htmlspecialchars($subtitle, ENT_QUOTES, 'UTF-8') . '
';
+ }
+ $html .= '';
+ return $html;
+});
+
return function (Router $router): void {
$controller = new PagesController();
$router->get('/page', [$controller, 'show']);
diff --git a/plugins/artists/plugin.php b/plugins/artists/plugin.php
index be51e81..0db69e8 100644
--- a/plugins/artists/plugin.php
+++ b/plugins/artists/plugin.php
@@ -2,10 +2,60 @@
declare(strict_types=1);
use Core\Http\Router;
+use Core\Services\Database;
+use Core\Services\Shortcodes;
use Plugins\Artists\ArtistsController;
require_once __DIR__ . '/ArtistsController.php';
+Shortcodes::register('new-artists', static function (array $attrs = []): string {
+ $limit = max(1, min(20, (int)($attrs['limit'] ?? 6)));
+ $db = Database::get();
+ if (!($db instanceof \PDO)) {
+ return '';
+ }
+
+ try {
+ $stmt = $db->prepare("
+ SELECT name, slug, country, avatar_url
+ FROM ac_artists
+ WHERE is_active = 1
+ ORDER BY created_at DESC, id DESC
+ LIMIT :limit
+ ");
+ $stmt->bindValue(':limit', $limit, \PDO::PARAM_INT);
+ $stmt->execute();
+ $rows = $stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [];
+ } catch (\Throwable $e) {
+ return '';
+ }
+
+ if (!$rows) {
+ return '
No artists available yet.
';
+ }
+
+ $cards = '';
+ foreach ($rows as $row) {
+ $name = htmlspecialchars((string)($row['name'] ?? ''), ENT_QUOTES, 'UTF-8');
+ $slug = rawurlencode((string)($row['slug'] ?? ''));
+ $country = htmlspecialchars(trim((string)($row['country'] ?? '')), ENT_QUOTES, 'UTF-8');
+ $avatar = trim((string)($row['avatar_url'] ?? ''));
+ $avatarHtml = $avatar !== ''
+ ? '
 . ')
'
+ : '
AC
';
+
+ $cards .= '
'
+ . '' . $avatarHtml . '
'
+ . ''
+ . '';
+ }
+
+ return '
';
+});
+
return function (Router $router): void {
$controller = new ArtistsController();
$router->get('/artists', [$controller, 'index']);
diff --git a/plugins/releases/plugin.php b/plugins/releases/plugin.php
index 0e7daea..1c4a57a 100644
--- a/plugins/releases/plugin.php
+++ b/plugins/releases/plugin.php
@@ -78,6 +78,10 @@ Shortcodes::register('releases', static function (array $attrs = []): string {
return '
';
});
+Shortcodes::register('latest-releases', static function (array $attrs = []): string {
+ return Shortcodes::render('[releases limit="' . max(1, min(20, (int)($attrs['limit'] ?? 8))) . '"]');
+});
+
return function (Router $router): void {
$controller = new ReleasesController();
$router->get('/releases', [$controller, 'index']);
diff --git a/plugins/store/plugin.php b/plugins/store/plugin.php
index 95aad91..c297d1c 100644
--- a/plugins/store/plugin.php
+++ b/plugins/store/plugin.php
@@ -39,18 +39,9 @@ Shortcodes::register('sale-chart', static function (array $attrs = []): string {
}
$rows = [];
- try {
- $latestPaid = (string)($db->query("SELECT MAX(updated_at) FROM ac_store_orders WHERE status = 'paid'")->fetchColumn() ?? '');
- $latestCache = (string)($db->query("SELECT MAX(updated_at) FROM ac_store_sales_chart_cache")->fetchColumn() ?? '');
- if ($latestPaid !== '' && ($latestCache === '' || strcmp($latestPaid, $latestCache) > 0)) {
- (new StoreController())->rebuildSalesChartCache();
- }
- } catch (\Throwable $e) {
- }
-
try {
$stmt = $db->prepare("
- SELECT item_key, item_label AS title, units, revenue
+ SELECT item_label AS title, units, revenue
FROM ac_store_sales_chart_cache
WHERE chart_scope = :scope
AND chart_window = :window
@@ -66,16 +57,19 @@ Shortcodes::register('sale-chart', static function (array $attrs = []): string {
$rows = [];
}
- if (!$rows && $scope === 'tracks') {
+ if (!$rows) {
try {
+ $controller = new StoreController();
+ $controller->rebuildSalesChartCache();
$stmt = $db->prepare("
- SELECT item_key, item_label AS title, units, revenue
+ SELECT item_label AS title, units, revenue
FROM ac_store_sales_chart_cache
- WHERE chart_scope = 'releases'
+ WHERE chart_scope = :scope
AND chart_window = :window
ORDER BY rank_no ASC
LIMIT :limit
");
+ $stmt->bindValue(':scope', $scope, \PDO::PARAM_STR);
$stmt->bindValue(':window', $window, \PDO::PARAM_STR);
$stmt->bindValue(':limit', $limit, \PDO::PARAM_INT);
$stmt->execute();
@@ -89,98 +83,33 @@ Shortcodes::register('sale-chart', static function (array $attrs = []): string {
return '
No sales yet.
';
}
- $releaseIds = [];
- $trackIds = [];
- foreach ($rows as $row) {
- $itemKey = trim((string)($row['item_key'] ?? ''));
- if (preg_match('/^release:(\d+)$/', $itemKey, $m)) {
- $releaseIds[] = (int)$m[1];
- } elseif (preg_match('/^track:(\d+)$/', $itemKey, $m)) {
- $trackIds[] = (int)$m[1];
- }
- }
- $releaseIds = array_values(array_unique(array_filter($releaseIds)));
- $trackIds = array_values(array_unique(array_filter($trackIds)));
-
- $releaseMap = [];
- if ($releaseIds) {
- try {
- $in = implode(',', array_fill(0, count($releaseIds), '?'));
- $stmt = $db->prepare("
- SELECT id, title, slug, cover_url, COALESCE(artist_name, '') AS artist_name
- FROM ac_releases
- WHERE id IN ({$in})
- ");
- foreach ($releaseIds as $i => $rid) {
- $stmt->bindValue($i + 1, $rid, \PDO::PARAM_INT);
- }
- $stmt->execute();
- $rels = $stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [];
- foreach ($rels as $rel) {
- $releaseMap['release:' . (int)$rel['id']] = $rel;
- }
- } catch (\Throwable $e) {
- }
- }
-
- if ($trackIds) {
- try {
- $in = implode(',', array_fill(0, count($trackIds), '?'));
- $stmt = $db->prepare("
- SELECT t.id AS track_id, r.id, r.title, r.slug, r.cover_url, COALESCE(r.artist_name, '') AS artist_name
- FROM ac_release_tracks t
- JOIN ac_releases r ON r.id = t.release_id
- WHERE t.id IN ({$in})
- ");
- foreach ($trackIds as $i => $tid) {
- $stmt->bindValue($i + 1, $tid, \PDO::PARAM_INT);
- }
- $stmt->execute();
- $rels = $stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [];
- foreach ($rels as $rel) {
- $releaseMap['track:' . (int)$rel['track_id']] = $rel;
- }
- } catch (\Throwable $e) {
- }
+ $currency = strtoupper(trim((string)Settings::get('store_currency', 'GBP')));
+ if (!preg_match('/^[A-Z]{3}$/', $currency)) {
+ $currency = 'GBP';
}
$list = '';
$position = 1;
foreach ($rows as $row) {
- $itemKey = trim((string)($row['item_key'] ?? ''));
- $rel = $releaseMap[$itemKey] ?? null;
-
- $titleRaw = $rel ? (string)($rel['title'] ?? '') : (string)($row['title'] ?? '');
- $artistRaw = $rel ? trim((string)($rel['artist_name'] ?? '')) : '';
- $slugRaw = $rel ? trim((string)($rel['slug'] ?? '')) : '';
- $coverRaw = $rel ? trim((string)($rel['cover_url'] ?? '')) : '';
-
- $title = htmlspecialchars($titleRaw !== '' ? $titleRaw : ((string)($row['title'] ?? 'Release')), ENT_QUOTES, 'UTF-8');
- $artist = htmlspecialchars($artistRaw, ENT_QUOTES, 'UTF-8');
- $href = $slugRaw !== '' ? '/release?slug=' . rawurlencode($slugRaw) : '#';
-
- $thumbHtml = $coverRaw !== ''
- ? '
 . ')
'
- : '
AC
';
-
- $copy = '
'
+ $title = htmlspecialchars((string)($row['title'] ?? ''), ENT_QUOTES, 'UTF-8');
+ $units = (int)($row['units'] ?? 0);
+ $revenue = number_format((float)($row['revenue'] ?? 0), 2);
+ $list .= ''
+ . '#' . $position . ''
. '' . $title . ''
- . ($artist !== '' ? '' . $artist . '' : '')
- . ''
- . '
' . $position . '';
-
- $content = '
' . $thumbHtml . '' . $copy;
-
- if ($href !== '#') {
- $content = '
' . $content . '';
- }
-
- $list .= '
' . $content . '';
+ . '
' . $units . ' sold - ' . htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') . ' ' . $revenue . ''
+ . '';
$position++;
}
- $heading = htmlspecialchars((string)($attrs['title'] ?? 'Top Sellers'), ENT_QUOTES, 'UTF-8');
- return '
';
+ return '
';
+});
+
+Shortcodes::register('top-sellers', static function (array $attrs = []): string {
+ $type = trim((string)($attrs['type'] ?? 'tracks'));
+ $window = trim((string)($attrs['window'] ?? 'latest'));
+ $limit = max(1, min(50, (int)($attrs['limit'] ?? 10)));
+ return Shortcodes::render('[sale-chart type="' . $type . '" window="' . $window . '" limit="' . $limit . '"]');
});
Shortcodes::register('login-link', static function (array $attrs = []): string {
diff --git a/views/site/layout.php b/views/site/layout.php
index a57f094..ab3a462 100644
--- a/views/site/layout.php
+++ b/views/site/layout.php
@@ -13,6 +13,7 @@ $seoDefaultDescription = trim(Settings::get('seo_meta_description', ''));
$seoRobotsIndex = Settings::get('seo_robots_index', '1') === '1' ? 'index' : 'noindex';
$seoRobotsFollow = Settings::get('seo_robots_follow', '1') === '1' ? 'follow' : 'nofollow';
$seoOgImage = trim(Settings::get('seo_og_image', ''));
+$siteCustomCss = (string)Settings::get('site_custom_css', '');
$pageTitleValue = trim((string)($pageTitle ?? $siteTitleSetting));
$metaTitle = $pageTitleValue;
if ($seoTitleSuffix !== '' && stripos($pageTitleValue, $seoTitleSuffix) === false) {
@@ -330,148 +331,134 @@ if (session_status() === PHP_SESSION_ACTIVE && isset($_SESSION['ac_site_notice']
color: var(--muted);
font-size: 12px;
}
- .ac-shortcode-sale-chart {
+ .ac-shortcode-artists-grid {
display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
}
- .ac-shortcode-sale-head h3 {
- margin: 0;
+ .ac-shortcode-artist-card {
+ text-decoration: none;
+ color: inherit;
+ border: 1px solid rgba(255,255,255,0.1);
+ background: rgba(15,18,24,0.6);
+ border-radius: 14px;
+ overflow: hidden;
+ display: grid;
+ min-height: 100%;
+ }
+ .ac-shortcode-artist-avatar {
+ aspect-ratio: 1 / 1;
+ background: rgba(255,255,255,0.03);
+ display: grid;
+ place-items: center;
+ overflow: hidden;
+ }
+ .ac-shortcode-artist-avatar img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
+ }
+ .ac-shortcode-artist-meta {
+ padding: 10px;
+ display: grid;
+ gap: 4px;
+ }
+ .ac-shortcode-artist-name {
+ font-size: 18px;
+ line-height: 1.2;
+ font-weight: 600;
+ }
+ .ac-shortcode-artist-country {
+ color: var(--muted);
font-size: 12px;
- letter-spacing: 0.2em;
+ }
+ .ac-shortcode-hero {
+ border: 1px solid rgba(255,255,255,0.14);
+ border-radius: 18px;
+ padding: 18px;
+ background: linear-gradient(135deg, rgba(255,255,255,0.05), rgba(255,255,255,0.01));
+ display: grid;
+ gap: 10px;
+ }
+ .ac-shortcode-hero-eyebrow {
+ font-size: 10px;
+ letter-spacing: 0.24em;
text-transform: uppercase;
color: var(--muted);
font-family: 'IBM Plex Mono', monospace;
- font-weight: 600;
+ }
+ .ac-shortcode-hero-title {
+ font-size: clamp(30px, 5vw, 54px);
+ line-height: 1.05;
+ font-weight: 700;
+ margin: 0;
+ }
+ .ac-shortcode-hero-subtitle {
+ font-size: 15px;
+ color: #d1d7e7;
+ max-width: 72ch;
+ margin: 0;
+ }
+ .ac-shortcode-hero-actions {
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+ margin-top: 4px;
+ }
+ .ac-shortcode-hero-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ height: 38px;
+ padding: 0 14px;
+ border-radius: 999px;
+ border: 1px solid rgba(255,255,255,0.16);
+ background: rgba(255,255,255,0.05);
+ color: #f5f7ff;
+ text-decoration: none;
+ font-size: 11px;
+ letter-spacing: 0.16em;
+ text-transform: uppercase;
+ font-family: 'IBM Plex Mono', monospace;
+ }
+ .ac-shortcode-hero-btn.primary {
+ border-color: rgba(57,244,179,0.6);
+ background: rgba(57,244,179,0.16);
+ color: #9ff8d8;
}
.ac-shortcode-sale-list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
- gap: 10px;
+ gap: 8px;
}
.ac-shortcode-sale-item {
border: 1px solid rgba(255,255,255,0.1);
- background: linear-gradient(180deg, rgba(15,18,24,0.78), rgba(12,14,20,0.72));
- border-radius: 12px;
- padding: 8px;
- transition: border-color .2s ease, box-shadow .2s ease, transform .2s ease;
- }
- .ac-shortcode-sale-item:hover {
- border-color: rgba(255,255,255,0.18);
- box-shadow: 0 10px 24px rgba(0,0,0,0.24);
- transform: translateY(-1px);
- }
- .ac-shortcode-sale-link {
- display: grid;
- grid-template-columns: auto 56px minmax(0, 1fr);
- gap: 12px;
- align-items: center;
- color: inherit;
- text-decoration: none;
- }
- .ac-shortcode-sale-link:hover .ac-shortcode-sale-title {
- color: #ffffff;
- }
- .ac-shortcode-sale-link > .ac-shortcode-sale-rank {
- order: 0;
- justify-self: start;
- min-width: 26px;
- text-align: left;
- margin-left: 2px;
- position: relative;
- padding-right: 18px;
- margin-right: 6px;
- }
- .ac-shortcode-sale-link > .ac-shortcode-sale-rank::before {
- content: '';
- position: absolute;
- right: 0;
- top: 50%;
- transform: translateY(-50%);
- width: 1px;
- height: 34px;
- background: linear-gradient(180deg, rgba(255,255,255,0.18) 0%, rgba(255,255,255,0.45) 50%, rgba(255,255,255,0.18) 100%);
- box-shadow: 0 0 12px rgba(255,255,255,0.08);
- border-radius: 1px;
- }
- .ac-shortcode-sale-link > .ac-shortcode-sale-rank::after {
- right: 10px;
- }
- .ac-shortcode-sale-link > .ac-shortcode-sale-thumb {
- order: 1;
- }
- .ac-shortcode-sale-link > .ac-shortcode-sale-copy {
- order: 2;
- }
-
- .ac-shortcode-sale-thumb {
- width: 56px;
- height: 56px;
+ background: rgba(15,18,24,0.6);
border-radius: 10px;
- overflow: hidden;
- background: rgba(255,255,255,0.04);
+ padding: 10px 12px;
display: grid;
- place-items: center;
- }
- .ac-shortcode-sale-thumb img {
- width: 100%;
- height: 100%;
- object-fit: cover;
- display: block;
- }
- .ac-shortcode-sale-copy {
- display: grid;
- gap: 2px;
- min-width: 0;
+ grid-template-columns: auto 1fr auto;
+ gap: 10px;
+ align-items: center;
}
.ac-shortcode-sale-rank {
font-family: 'IBM Plex Mono', monospace;
- font-size: 28px;
- line-height: 1;
- font-weight: 700;
- letter-spacing: -0.02em;
- min-width: 34px;
- text-align: right;
- position: relative;
- display: inline-block;
- background: linear-gradient(180deg, rgba(245,247,255,0.95) 0%, rgba(169,178,196,0.72) 100%);
- -webkit-background-clip: text;
- background-clip: text;
- -webkit-text-fill-color: transparent;
- text-shadow: 0 10px 22px rgba(0,0,0,0.40);
- transition: transform .2s ease, filter .2s ease;
- }
- .ac-shortcode-sale-rank::after {
- content: '.';
- color: var(--accent);
- position: absolute;
- right: -7px;
- bottom: -1px;
- font-size: 16px;
- font-weight: 700;
- line-height: 1;
- text-shadow: none;
- -webkit-text-fill-color: initial;
- background: none;
- }
- .ac-shortcode-sale-link:hover .ac-shortcode-sale-rank {
- transform: translateY(-1px) scale(1.03);
- filter: brightness(1.08);
+ font-size: 11px;
+ color: var(--muted);
+ letter-spacing: 0.15em;
}
.ac-shortcode-sale-title {
- font-size: 16px;
- line-height: 1.25;
- font-weight: 700;
+ font-size: 14px;
+ line-height: 1.3;
}
-
- .ac-shortcode-sale-artist {
+ .ac-shortcode-sale-meta {
font-size: 12px;
color: var(--muted);
- line-height: 1.25;
- letter-spacing: 0.02em;
+ white-space: nowrap;
}
-
@media (max-width: 900px) {
.shell {
padding: 12px 14px 18px;
@@ -540,6 +527,9 @@ if (session_status() === PHP_SESSION_ACTIVE && isset($_SESSION['ac_site_notice']
padding-top: 2px;
}
}
+
+= str_replace('
+
@@ -558,20 +548,5 @@ if (session_status() === PHP_SESSION_ACTIVE && isset($_SESSION['ac_site_notice']
= $content ?? '' ?>
-
-
-