<?php // station.php – Station detail (Bulma + PJAX + New RadioMouse UI) declare(strict_types=1); require_once __DIR__ . '/includes/db.php'; require_once __DIR__ . '/includes/helpers.php'; require_once __DIR__ . '/includes/settings.php'; if (!function_exists('h')) { function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); } } $pdo = db(); $siteName = get_setting('site_name', 'RadioMouse'); /* ------------------------------------------------- Load station by slug or fallback ID ------------------------------------------------- */ $slug = trim($_GET['slug'] ?? ''); $station = null; if ($slug !== '') { $st = $pdo->prepare("SELECT * FROM stations WHERE slug = ? AND is_active = 1 LIMIT 1"); $st->execute([$slug]); $station = $st->fetch(PDO::FETCH_ASSOC); } else { $id = isset($_GET['id']) ? (int)$_GET['id'] : 0; if ($id > 0) { $st = $pdo->prepare("SELECT * FROM stations WHERE id = ? AND is_active = 1 LIMIT 1"); $st->execute([$id]); $station = $st->fetch(PDO::FETCH_ASSOC); } } if (!$station) { http_response_code(404); exit('Station not found'); } $stationId = (int)$station['id']; $slugReal = (string)($station['slug'] ?? ''); $href = $slugReal ? '/station/' . $slugReal : '/station/' . $stationId; $name = (string)$station['name']; $country = trim((string)($station['country'] ?? '')); $lang = trim((string)($station['language'] ?? '')); $genre = trim((string)($station['genre'] ?? '')); $bitrate = (int)($station['bitrate'] ?? 0); $website = trim((string)($station['website_url'] ?? '')); // Enrichment fields $description = trim((string)($station['description'] ?? '')); $descriptionSrc = trim((string)($station['description_src'] ?? '')); $pageTitle = trim((string)($station['page_title'] ?? '')); /* Normalize logo */ $logoRaw = (string)($station['logo'] ?? ''); $logo = $logoRaw; if ($logo !== '' && strncasecmp($logo, "http://", 7) === 0) { $logo = "https://" . substr($logo, 7); } /* ---------------------------------------------- Related stations ---------------------------------------------- */ $related = []; try { $rel = $pdo->prepare(" SELECT id, slug, name, logo, country, genre, bitrate FROM stations WHERE is_active = 1 AND id <> ? AND ( (country <> '' AND country = ?) OR (genre <> '' AND genre = ?) ) ORDER BY featured DESC, name ASC LIMIT 10 "); $rel->execute([$stationId, $country, $genre]); $related = $rel->fetchAll(PDO::FETCH_ASSOC); } catch (Throwable $e) { $related = []; } /* ---------------------------------------------- Meta for layout – use enrichment when present ---------------------------------------------- */ $page = 'station'; if ($pageTitle !== '') { $metaTitle = $pageTitle; } else { $metaTitle = $name . " – " . $siteName; } if ($description !== '') { $short = mb_substr($description, 0, 160); if (mb_strlen($description) > 160) { $short .= '…'; } $metaDesc = $short; } else { $metaDesc = "Listen to " . $name . " and similar stations on " . $siteName . "."; } // ---- SEO: canonical URL, OG image, JSON-LD ---- $base = rtrim(base_url(), '/'); $metaUrl = $base . $href; $metaType = 'music.radio_station'; // Build absolute logo URL for social cards $metaImage = ''; if ($logo !== '') { if (strpos($logo, '//') === 0) { $metaImage = 'https:' . $logo; } elseif (preg_match('~^https?://~i', $logo)) { $metaImage = $logo; } else { $metaImage = $base . '/' . ltrim($logo, '/'); } } else { $fallbackImage = (string)get_setting('seo_default_image', ''); if ($fallbackImage !== '') { $metaImage = $fallbackImage; } } // JSON-LD RadioStation schema $schema = [ '@context' => 'https://schema.org', '@type' => 'RadioStation', 'name' => $name, 'url' => $metaUrl, 'image' => $metaImage ?: null, 'areaServed' => $country ?: null, 'inLanguage' => $lang ?: null, 'genre' => $genre ?: null, ]; $schema = array_filter($schema, static fn($v) => $v !== null && $v !== ''); $metaExtra = '<script type="application/ld+json">' . json_encode($schema, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . '</script>'; /* ---------------------------------------------- Social share helpers ---------------------------------------------- */ $shareUrl = $metaUrl; $shareText = $name . ' – ' . $siteName; $shareUrlEncoded = rawurlencode($shareUrl); $shareTextEncoded = rawurlencode($shareText); $whatsTextEncoded = rawurlencode($shareText . ' ' . $shareUrl); ob_start(); ?> <style> /* ============================================================ STATION HERO ============================================================ */ .rm-station-hero { padding: 2rem 2rem; border-radius: 18px; background: linear-gradient(160deg, #161616 0%, #050505 100%); border: 1px solid #252525; box-shadow: 0 18px 48px rgba(0,0,0,0.45); margin-bottom: 2rem; } .rm-station-thumb { width: 230px; height: 230px; border-radius: 20px; overflow: hidden; background: #111; display: flex; align-items: center; justify-content: center; } .rm-station-thumb img { width: 100%; height: 100%; object-fit: cover; } /* CLAMP LONG TITLES */ .rm-station-title { font-size: 2.2rem; font-weight: 700; margin-bottom: 0.6rem; max-width: 100%; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis; word-break: break-word; } /* ============================================================ META BOX ============================================================ */ .rm-station-meta-box { margin-top: 1rem; padding: 1rem 1.2rem; background: radial-gradient(circle at top left, #111827 0%, #020617 60%); border-radius: 14px; border: 1px solid rgba(148,163,184,0.35); color: #e5e7eb; } .rm-meta-line { font-size: 0.95rem; margin-bottom: 0.5rem; display: flex; flex-wrap: wrap; align-items: center; } .rm-station-meta-box .rm-meta-line strong { text-transform: uppercase; letter-spacing: 0.04em; font-size: 0.78rem; font-weight: 700; margin-right: 6px; opacity: 0.9; } .rm-station-meta-box .rm-meta-line:nth-child(1) strong { color: #f97316; } .rm-station-meta-box .rm-meta-line:nth-child(2) strong { color: #22c55e; } .rm-station-meta-box .rm-meta-line:nth-child(3) strong { color: #6366f1; } .rm-station-meta-box .rm-meta-line:nth-child(4) strong { color: #ec4899; } .rm-station-meta-box .rm-meta-line:nth-child(5) strong { color: #eab308; } .rm-station-meta-box .rm-meta-line span, .rm-station-meta-box .rm-meta-line a { font-size: 0.92rem; } .rm-station-meta-box .rm-meta-line a { color: #22c55e; text-decoration: none; border-bottom: 1px dashed rgba(34,197,94,0.45); } .rm-station-meta-box .rm-meta-line a:hover { color: #4ade80; border-bottom-color: rgba(74,222,128,0.9); } /* ============================================================ ABOUT BOX ============================================================ */ .rm-station-about { margin-bottom: 2rem; padding: 1.15rem 1.25rem; border-radius: 14px; background: #050505; border: 1px solid #1f2933; color: #e5e7eb; line-height: 1.6; font-size: 0.9rem; } .rm-station-about-title { font-size: 1.05rem; font-weight: 600; margin-bottom: 0.45rem; } .rm-station-about-body { white-space: pre-wrap; } .rm-station-about-source { margin-top: 0.5rem; } /* ============================================================ BIG PLAY + FAVORITE ============================================================ */ .rm-play-big { background: #32cd32 !important; font-weight: 700 !important; } .rm-play-big:hover { background: #42e542 !important; } .rm-fav-big { background: #ff3f7d !important; } .rm-fav-big.is-fav { background: #ff1760 !important; } .rm-station-hero .buttons { display: flex; flex-wrap: wrap; gap: 0.5rem; } /* ============================================================ SOCIAL SHARE STRIP ============================================================ */ .rm-station-share-row { margin-top: 0.85rem; display: flex; flex-wrap: wrap; align-items: center; gap: 0.55rem; } .rm-share-label { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.12em; color: #9ca3af; margin-right: 0.2rem; } .rm-share-chip, .rm-share-chip:visited { display: inline-flex; align-items: center; gap: 0.35rem; padding: 5px 10px; border-radius: 999px; font-size: 0.78rem; font-weight: 600; text-decoration: none; border: 0; cursor: pointer; color: #f9fafb; background: #1f2937; box-shadow: 0 6px 16px rgba(0,0,0,0.4); transition: transform 0.12s ease, box-shadow 0.12s ease, opacity 0.12s ease; white-space: nowrap; } .rm-share-chip:hover { transform: translateY(-1px); box-shadow: 0 10px 24px rgba(0,0,0,0.55); opacity: 0.95; } .rm-share-facebook { background: linear-gradient(135deg,#2563eb,#1d4ed8); } .rm-share-twitter { background: linear-gradient(135deg,#0f172a,#64748b); } .rm-share-whatsapp { background: linear-gradient(135deg,#22c55e,#15803d); } .rm-share-copy { background: linear-gradient(135deg,#4b5563,#111827); } .rm-share-chip span.rm-share-icon { font-size: 0.9rem; } /* ============================================================ RELATED CARDS – UPDATED THUMBNAILS (no crop / no blur) ============================================================ */ .rm-related-grid .rm-card, .rm-related-grid .rm-card-fav { display: flex; flex-direction: column; align-items: stretch; overflow: hidden; background: linear-gradient(135deg, #121212, #151515); border-radius: 16px; border: 1px solid rgba(255,255,255,0.03); padding: 0.8rem; transition: transform .12s, border-color .12s, box-shadow .12s; } .rm-related-grid .rm-card:hover { transform: translateY(-2px); border-color: rgba(46,125,50,0.35); box-shadow: 0 12px 32px rgba(0,0,0,0.45); } /* Thumb container: center content, no forced crop */ .rm-related-grid .rm-card-thumb { width: 100%; border-radius: 12px; background: radial-gradient(circle at top left, #232323, #141414); display: flex; align-items: center; justify-content: center; padding: 0.35rem; min-height: 120px; overflow: hidden; } /* Image: keep aspect ratio, no cropping, no stretching */ .rm-related-grid .rm-card-thumb img { width: auto; height: auto; max-width: 100%; max-height: 100%; object-fit: contain; image-rendering: auto; display: block; } /* Placeholder letter when no logo */ .rm-related-grid .rm-card-thumb .rm-thumb-placeholder { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; font-size: 1.6rem; color: var(--rm-muted); } .rm-related-grid .rm-card-info { padding-top: 0.55rem; } .rm-related-grid .rm-card-title { font-size: 0.92rem; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .rm-related-grid .rm-card-meta-pill { margin-top: 0.35rem; display: flex; flex-wrap: wrap; gap: 6px; } .rm-related-grid .rm-pill.rm-pill-country { max-width: 75%; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; } .rm-related-grid .rm-pill.rm-pill-bitrate { font-size: 0.7rem; } .rm-related-grid .rm-play-btn { position: absolute; top: 8px; left: 8px; width: 22px; height: 22px; font-size: 11px; background: rgba(0,0,0,0.7); border-radius: 50%; border: 0; color: #fff; z-index: 3; } .rm-related-grid .rm-fav-btn { position: absolute; top: 8px; right: 8px; font-size: 1.1rem; background: transparent; color: var(--rm-muted); border: 0; } .rm-related-title { font-size: 1.25rem; font-weight: 600; margin-bottom: 0.6rem; } /* ============================================================ MOBILE OPTIMIZATIONS ============================================================ */ @media (max-width: 768px) { .rm-station-hero { padding: 1.2rem 1.1rem; margin-bottom: 1.4rem; } .rm-station-hero .columns { margin-left: 0; margin-right: 0; } .rm-station-hero .column.is-narrow { display: flex; justify-content: center; margin-bottom: 0.75rem; } .rm-station-thumb { width: 160px; height: 160px; } .rm-station-title { font-size: 1.6rem; text-align: center; } .rm-station-hero .column:last-child { text-align: center; } .rm-station-hero .buttons { justify-content: center; margin-top: 0.6rem; } .rm-station-hero .buttons .button { flex: 1 1 48%; min-width: 130px; font-size: 0.85rem; } .rm-station-share-row { justify-content: center; } .rm-station-meta-box { margin-top: 0.9rem; padding: 0.85rem 0.9rem; } .rm-meta-line { font-size: 0.85rem; margin-bottom: 0.35rem; } .rm-station-meta-box .rm-meta-line span, .rm-station-meta-box .rm-meta-line a { font-size: 0.84rem; } .rm-station-about { padding: 0.95rem 0.95rem; font-size: 0.86rem; } .rm-related-grid { display: flex; flex-wrap: wrap; margin-left: -0.35rem; margin-right: -0.35rem; } .rm-related-grid > .column.is-half-mobile { flex: 0 0 50%; max-width: 50%; padding-left: 0.35rem; padding-right: 0.35rem; margin-bottom: 0.7rem; } .rm-related-grid .rm-card, .rm-related-grid .rm-card-fav { padding: 0.55rem 0.55rem 0.6rem; border-radius: 12px; } /* Slightly shorter thumbs on mobile but same behaviour */ .rm-related-grid .rm-card-thumb { min-height: 100px; padding: 0.3rem; } .rm-related-grid .rm-card-title { font-size: 0.8rem; } .rm-related-grid .rm-pill { font-size: 0.62rem; padding: 2px 6px; } .rm-related-grid .rm-play-btn { top: 6px; left: 6px; width: 20px; height: 20px; font-size: 10px; } .rm-related-grid .rm-fav-btn { top: 6px; right: 6px; font-size: 0.95rem; } } </style> <!-- Hidden meta for footer player --> <div id="rm-station-meta" data-station-id="<?= $stationId ?>" data-name="<?= h($name) ?>" data-logo="<?= h($logo) ?>" data-url="<?= h($href) ?>"></div> <section class="section rm-section-tight"> <div class="container"> <!-- HERO --> <div class="rm-station-hero"> <div class="columns is-vcentered is-variable is-6"> <!-- Left: Logo --> <div class="column is-narrow has-text-centered"> <div class="rm-station-thumb"> <?php if ($logo): ?> <img src="<?= h($logo) ?>" alt="<?= h($name) ?>" style="width:100%;height:100%;object-fit:cover;"> <?php else: ?> <div class="rm-thumb-placeholder" style="font-size:3.5rem;"> <?= h(mb_strtoupper(mb_substr($name,0,1))) ?> </div> <?php endif; ?> </div> </div> <!-- Right: Info --> <div class="column"> <h1 class="rm-station-title" title="<?= h($name) ?>"><?= h($name) ?></h1> <div class="buttons"> <button class="button is-medium is-rounded rm-play-big rm-play-btn" data-play="<?= $stationId ?>" onclick="event.preventDefault(); event.stopPropagation();"> ▶ Play station </button> <button class="button is-medium is-rounded rm-fav-big rm-fav-btn" data-fav="<?= $stationId ?>" onclick="event.preventDefault(); event.stopPropagation();"> ♡ Favorite </button> </div> <!-- Colorful social share row --> <div class="rm-station-share-row"> <span class="rm-share-label">Share</span> <a class="rm-share-chip rm-share-facebook" href="https://www.facebook.com/sharer/sharer.php?u=<?= $shareUrlEncoded ?>" target="_blank" rel="noopener"> <span class="rm-share-icon">📘</span> <span>Facebook</span> </a> <a class="rm-share-chip rm-share-twitter" href="https://twitter.com/intent/tweet?url=<?= $shareUrlEncoded ?>&text=<?= $shareTextEncoded ?>" target="_blank" rel="noopener"> <span class="rm-share-icon">✕</span> <span>Twitter</span> </a> <a class="rm-share-chip rm-share-whatsapp" href="https://api.whatsapp.com/send?text=<?= h($whatsTextEncoded) ?>" target="_blank" rel="noopener"> <span class="rm-share-icon">💬</span> <span>WhatsApp</span> </a> <button type="button" class="rm-share-chip rm-share-copy" data-share-copy="<?= h($shareUrl) ?>"> <span class="rm-share-icon">📋</span> <span>Copy link</span> </button> </div> <div class="rm-station-meta-box"> <?php if ($country): ?> <div class="rm-meta-line"> <strong>Country:</strong> <span><?= h($country) ?></span> </div> <?php endif; ?> <?php if ($lang): ?> <div class="rm-meta-line"> <strong>Language:</strong> <span><?= h($lang) ?></span> </div> <?php endif; ?> <?php if ($genre): ?> <div class="rm-meta-line"> <strong>Genre:</strong> <span><?= h($genre) ?></span> </div> <?php endif; ?> <?php if ($bitrate): ?> <div class="rm-meta-line"> <strong>Bitrate:</strong> <span><?= $bitrate ?> kbps</span> </div> <?php endif; ?> <?php if ($website): ?> <div class="rm-meta-line"> <strong>Website:</strong> <a href="<?= h($website) ?>" target="_blank" rel="noopener"> <?= h(parse_url($website, PHP_URL_HOST) ?: $website) ?> </a> </div> <?php endif; ?> </div> </div> </div> </div> <!-- ABOUT / DESCRIPTION --> <?php if ($description !== ''): ?> <div class="rm-station-about"> <div class="rm-station-about-title">About this station</div> <div class="rm-station-about-body"> <?= nl2br(h($description)) ?> </div> <div class="rm-station-about-source"> <span class="tag is-info is-light is-rounded is-size-7"> Source: <?php $srcLabel = $descriptionSrc ?: 'manual'; if ($srcLabel === 'generated') $srcLabel = 'auto-generated'; echo h(ucfirst($srcLabel)); ?> </span> </div> </div> <?php else: ?> <div class="rm-station-about"> <div class="rm-station-about-title">About this station</div> <p class="rm-station-about-body rm-muted"> This station doesn't have a description yet. Stay tuned – the mouse is still enriching it. </p> </div> <?php endif; ?> <!-- RELATED STATIONS --> <?php if ($related): ?> <h2 class="rm-related-title">You might also like</h2> <div class="columns is-multiline is-variable is-3 rm-home-grid rm-related-grid"> <?php foreach ($related as $r): ?> <?php $rid = (int)$r['id']; $rslug = (string)($r['slug'] ?? ''); $rHref = $rslug ? '/station/' . h($rslug) : '/station/' . $rid; $rLogo = $r['logo'] ?? ''; if ($rLogo && strncasecmp($rLogo, "http://", 7) === 0) { $rLogo = "https://" . substr($rLogo, 7); } ?> <div class="column is-one-quarter-desktop is-one-third-tablet is-half-mobile"> <div class="rm-card rm-card-fav" data-station-id="<?= $rid ?>" data-name="<?= h($r['name']) ?>" data-logo="<?= h($rLogo) ?>" data-url="<?= h($rHref) ?>"> <!-- Play --> <button class="rm-play-btn" data-play="<?= $rid ?>" onclick="event.preventDefault(); event.stopPropagation();">▶</button> <a href="<?= $rHref ?>" class="rm-card-link"> <div class="rm-card-thumb"> <?php if ($rLogo): ?> <img src="<?= h($rLogo) ?>" alt="<?= h($r['name']) ?>" loading="lazy"> <?php else: ?> <div class="rm-thumb-placeholder"> <?= h(mb_strtoupper(mb_substr($r['name'],0,1))) ?> </div> <?php endif; ?> </div> <div class="rm-card-info"> <div class="rm-card-title"><?= h($r['name']) ?></div> <div class="rm-card-meta-pill"> <?php if (!empty($r['country'])): ?> <span class="rm-pill rm-pill-country"><?= h($r['country']) ?></span> <?php endif; ?> <?php if (!empty($r['bitrate'])): ?> <span class="rm-pill rm-pill-bitrate"><?= (int)$r['bitrate'] ?> kbps</span> <?php endif; ?> </div> </div> </a> <!-- Favorite --> <button class="rm-fav-btn" data-fav="<?= $rid ?>" onclick="event.preventDefault(); event.stopPropagation();">♡</button> </div> </div> <?php endforeach; ?> </div> <?php endif; ?> </div> </section> <script> // Copy-link share button document.addEventListener('click', function (e) { const btn = e.target.closest('.rm-share-copy'); if (!btn) return; const url = btn.getAttribute('data-share-copy') || window.location.href; if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(url).then(function () { const original = btn.innerHTML; btn.innerHTML = '<span class="rm-share-icon">✅</span><span>Copied</span>'; setTimeout(function () { btn.innerHTML = original; }, 1500); }).catch(function () { alert('Link copied: ' + url); }); } else { alert('Link copied: ' + url); } }); </script> <?php $content = ob_get_clean(); include __DIR__ . '/partials/template.php';