This commit is contained in:
2025-06-22 19:34:42 -04:00
parent 8523b14aae
commit 0a0eaeb712
2 changed files with 416 additions and 66 deletions

View File

@ -11,6 +11,9 @@
background-color: black;
color: white;
font-family: 'Metal Mania', sans-serif;
position: relative;
min-height: 100vh;
padding-bottom: 100px;
}
.card {
@ -18,20 +21,69 @@
border: 1px solid #444;
border-radius: 8px;
overflow: hidden;
transition: transform 0.3s ease;
}
.card:hover {
transform: translateY(-5px);
}
.card-body {
padding: 1.5rem;
}
.btn {
background-color: #ff5500;
border-color: #ff5500;
color: white;
font-weight: bold;
transition: background-color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease;
}
.btn:hover {
background-color: #ff3300;
border-color: #ff3300;
color: white;
}
.btn:active,
.btn:focus {
background-color: #cc4400;
border-color: #cc4400;
box-shadow: 0 0 8px rgba(255, 85, 0, 0.5);
outline: none;
}
.btn:disabled,
.btn[disabled] {
background-color: #444;
border-color: #444;
color: #888;
opacity: 1;
cursor: not-allowed;
}
.btn-primary {
background-color: #ff5500;
border-color: #ff5500;
transition: background-color 0.3s ease;
}
.btn-primary:hover {
background-color: #ff3300;
border-color: #ff3300;
}
.btn-primary:active,
.btn-primary:focus {
background-color: #cc4400;
border-color: #cc4400;
}
.btn-primary:disabled,
.btn-primary[disabled] {
background-color: #444;
border-color: #444;
color: #888;
}
.pagination-btn {
@ -154,35 +206,6 @@
margin-left: -15px;
}
.pagination-btn {
margin: 0 10px;
color: white;
font-weight: bold;
}
.pagination-btn.btn-primary:hover {
background-color: #ff3300;
border-color: #ff3300;
}
.pagination-btn.btn-primary:disabled,
.pagination-btn.btn-primary[disabled] {
background-color: #444;
border-color: #444;
color: #888;
opacity: 1;
cursor: not-allowed;
}
.pagination-btn.btn-primary:active,
.pagination-btn.btn-primary:focus {
background-color: #cc4400;
border-color: #cc4400;
box-shadow: 0 0 8px rgba(255, 85, 0, 0.5);
outline: none;
}
/* Search box styling */
.search-container {
position: relative;
width: 100%;
@ -231,7 +254,8 @@
transition: background-color 0.2s ease;
}
.search-result-item:hover {
.search-result-item:hover,
.search-result-item.selected {
background-color: #ff5500;
}
@ -239,6 +263,73 @@
color: #aaa;
display: block;
}
.loading-spinner {
display: none;
width: 24px;
height: 24px;
border: 3px solid #ff5500;
border-top: 3px solid transparent;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto;
}
.loading-spinner.show {
display: block;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
footer {
background-color: #1e1e1e;
color: #aaa;
padding: 2rem 0;
text-align: center;
position: absolute;
bottom: 0;
width: 100%;
border-top: 2px solid #ff5500;
}
footer a {
color: #ff5500;
text-decoration: none;
transition: color 0.3s ease;
}
footer a:hover {
color: #ff3300;
}
.back-to-top {
position: fixed;
bottom: 20px;
right: 20px;
background-color: #ff5500;
color: white;
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
text-decoration: none;
opacity: 0;
transition: opacity 0.3s ease, background-color 0.3s ease;
z-index: 1000;
}
.back-to-top.show {
opacity: 1;
}
.back-to-top:hover {
background-color: #ff3300;
}
</style>
</head>
@ -260,7 +351,7 @@
<% } %>
</ul>
<div class="search-container ml-auto">
<input type="text" class="search-input" placeholder="Search tracks or lyrics..." aria-label="Search tracks">
<input type="text" class="search-input" placeholder="Search tracks or lyrics..." aria-label="Search tracks" autofocus>
<div class="search-results" id="search-results"></div>
</div>
</div>
@ -306,10 +397,21 @@
<button class="btn btn-primary pagination-btn" id="<%= genre %>-next"
data-page="<%= genreTracks[genre].page + 1 %>"
<%=genreTracks[genre].page===genreTracks[genre].totalPages ? 'disabled' : '' %>>Next</button>
<div class="loading-spinner" id="<%= genre %>-spinner"></div>
</div>
</section>
<% } %>
</div>
<!--
<footer>
<p>&copy; 2025 Raven Scott Music. All rights reserved.</p>
</footer> -->
<a href="#" class="back-to-top" id="back-to-top" title="Back to Top">
<svg width="24" height="24" fill="white" viewBox="0 0 24 24">
<path d="M12 2L3 11h6v10h6V11h6z"/>
</svg>
</a>
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.4/dist/umd/popper.min.js"></script>
@ -319,16 +421,21 @@
// Lazy load iframes
function lazyLoadIframes(container = document) {
const iframes = container.querySelectorAll('iframe[data-src]');
iframes.forEach(iframe => {
if (!iframe.src && iframe.getBoundingClientRect().top < window.innerHeight) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const iframe = entry.target;
iframe.src = iframe.getAttribute('data-src');
console.log(`Lazy loading iframe for track: ${iframe.closest('[data-slug]').dataset.slug}`);
observer.unobserve(iframe);
}
});
}, { rootMargin: '100px' });
iframes.forEach(iframe => observer.observe(iframe));
}
window.addEventListener('load', () => lazyLoadIframes());
window.addEventListener('scroll', () => lazyLoadIframes());
// Page cache: { genre: { page: { container, totalPages } } }
const pageCache = {};
@ -346,6 +453,8 @@
socket.on('page_data', ({ genre, tracks, page, totalPages }) => {
console.log(`Received page_data for genre: ${genre}, page: ${page}, tracks: ${tracks.length}`);
const trackContainer = document.getElementById(`${genre}-tracks`);
const spinner = document.getElementById(`${genre}-spinner`);
spinner.classList.remove('show');
if (!pageCache[genre]) {
pageCache[genre] = {};
@ -408,15 +517,18 @@
socket.on('error', ({ message }) => {
console.error('WebSocket error:', message);
document.querySelectorAll('.loading-spinner').forEach(spinner => spinner.classList.remove('show'));
});
document.querySelectorAll('.pagination-btn').forEach(button => {
button.addEventListener('click', () => {
const genre = button.id.split('-')[0];
const page = parseInt(button.dataset.page, 10);
const spinner = document.getElementById(`${genre}-spinner`);
console.log(`Button clicked: ${button.id}, genre: ${genre}, page: ${page}`);
if (page > 0 && page !== currentPages[genre]) {
spinner.classList.add('show');
if (pageCache[genre] && pageCache[genre][page]) {
console.log(`Loading cached page ${page} for genre: ${genre}`);
const trackContainer = document.getElementById(`${genre}-tracks`);
@ -437,14 +549,17 @@
currentPages[genre] = page;
lazyLoadIframes(pageCache[genre][page].container);
spinner.classList.remove('show');
} else {
socket.emit('request_page', { genre, page });
console.log(`Emitted request_page for genre: ${genre}, page: ${page}`);
}
} else if (page <= 0) {
console.error(`Invalid page number: ${page}`);
spinner.classList.remove('show');
} else {
console.log(`Already on page ${page} for genre: ${genre}`);
spinner.classList.remove('show');
}
});
});
@ -452,6 +567,7 @@
// Search functionality
const searchInput = document.querySelector('.search-input');
const searchResults = document.getElementById('search-results');
const searchContainer = document.querySelector('.search-container');
let allTracks = [];
const genres = ['metal', 'altrock', 'rap', 'lofi', 'edm', 'cuts'];
@ -471,10 +587,9 @@
}
}
// Initialize tracks on page load
fetchAllTracks();
// Debounce function to limit search frequency
// Debounce function
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
@ -507,6 +622,7 @@
filteredTracks.forEach(track => {
const resultItem = document.createElement('div');
resultItem.className = 'search-result-item';
resultItem.tabIndex = 0;
resultItem.innerHTML = `
${track.title}
<small>${track.genre.charAt(0).toUpperCase() + track.genre.slice(1)}</small>
@ -514,29 +630,84 @@
resultItem.addEventListener('click', () => {
window.location.href = `/${track.genre}/track/${track.slug}`;
});
resultItem.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
window.location.href = `/${track.genre}/track/${track.slug}`;
}
});
searchResults.appendChild(resultItem);
});
}
searchResults.classList.add('show');
updateSelectedResult();
}, 300);
// Search input event listener
searchInput.addEventListener('input', performSearch);
// Keyboard navigation for search results
function updateSelectedResult() {
const items = searchResults.querySelectorAll('.search-result-item');
items.forEach((item, index) => {
item.addEventListener('mouseover', () => {
items.forEach(i => i.classList.remove('selected'));
item.classList.add('selected');
selectedIndex = index;
});
});
}
// Hide search results when clicking outside
document.addEventListener('click', (e) => {
if (!searchContainer.contains(e.target)) {
searchResults.classList.remove('show');
let selectedIndex = -1;
searchInput.addEventListener('keydown', (e) => {
const items = searchResults.querySelectorAll('.search-result-item');
if (items.length === 0) return;
if (e.key === 'ArrowDown') {
e.preventDefault();
selectedIndex = Math.min(selectedIndex + 1, items.length - 1);
items.forEach(i => i.classList.remove('selected'));
items[selectedIndex].classList.add('selected');
items[selectedIndex].scrollIntoView({ block: 'nearest' });
} else if (e.key === 'ArrowUp') {
e.preventDefault();
selectedIndex = Math.max(selectedIndex - 1, -1);
items.forEach(i => i.classList.remove('selected'));
if (selectedIndex >= 0) {
items[selectedIndex].classList.add('selected');
items[selectedIndex].scrollIntoView({ block: 'nearest' });
}
} else if (e.key === 'Enter' && selectedIndex >= 0) {
e.preventDefault();
items[selectedIndex].click();
}
});
// Show search results when clicking input
searchInput.addEventListener('input', performSearch);
searchInput.addEventListener('focus', () => {
if (searchInput.value.trim().length >= 2) {
performSearch();
}
});
document.addEventListener('click', (e) => {
if (!searchContainer.contains(e.target)) {
searchResults.classList.remove('show');
selectedIndex = -1;
}
});
// Back to top button
const backToTop = document.getElementById('back-to-top');
window.addEventListener('scroll', () => {
if (window.scrollY > 300) {
backToTop.classList.add('show');
} else {
backToTop.classList.remove('show');
}
});
backToTop.addEventListener('click', (e) => {
e.preventDefault();
window.scrollTo({ top: 0, behavior: 'smooth' });
});
</script>
</body>

View File

@ -13,6 +13,9 @@
background-color: black;
color: white;
font-family: 'Metal Mania', sans-serif;
position: relative;
min-height: 100vh;
padding-bottom: 100px;
}
.card {
@ -20,20 +23,53 @@
border: 1px solid #444;
border-radius: 8px;
overflow: hidden;
transition: transform 0.3s ease;
}
.card:hover {
transform: translateY(-5px);
}
.card-body {
padding: 1.5rem;
}
.btn {
background-color: #ff5500;
border-color: #ff5500;
color: white;
font-weight: bold;
transition: background-color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease;
}
.btn:hover {
background-color: #ff3300;
border-color: #ff3300;
color: white;
}
.btn:active,
.btn:focus {
background-color: #cc4400;
border-color: #cc4400;
box-shadow: 0 0 8px rgba(255, 85, 0, 0.5);
outline: none;
}
.btn-primary {
background-color: #ff5500;
border-color: #ff5500;
transition: background-color 0.3s ease;
}
.btn-primary:hover {
background-color: #ff3300;
border-color: #ff3300;
}
.btn-primary:active,
.btn-primary:focus {
background-color: #cc4400;
border-color: #cc4400;
}
::-webkit-scrollbar {
@ -59,7 +95,6 @@
scrollbar-color: #4a4a4a #1e1e1e;
}
/* Header styling */
.site-header {
color: #ff5500;
font-size: 2.5rem;
@ -86,7 +121,6 @@
color: #ff3300;
}
/* Search box styling */
.search-container {
position: relative;
width: 100%;
@ -136,7 +170,8 @@
transition: background-color 0.2s ease;
}
.search-result-item:hover {
.search-result-item:hover,
.search-result-item.selected {
background-color: #ff5500;
}
@ -145,11 +180,68 @@
display: block;
}
/* Adjust container padding */
.container {
padding-top: 30px;
padding-bottom: 30px;
}
footer {
background-color: #1e1e1e;
color: #aaa;
padding: 2rem 0;
text-align: center;
position: absolute;
bottom: 0;
width: 100%;
border-top: 2px solid #ff5500;
}
footer a {
color: #ff5500;
text-decoration: none;
transition: color 0.3s ease;
}
footer a:hover {
color: #ff3300;
}
.back-to-top {
position: fixed;
bottom: 20px;
right: 20px;
background-color: #ff5500;
color: white;
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
text-decoration: none;
opacity: 0;
transition: opacity 0.3s ease, background-color 0.3s ease;
z-index: 1000;
}
.back-to-top.show {
opacity: 1;
}
.back-to-top:hover {
background-color: #ff3300;
}
.share-button {
background-color: #ff5500;
border-color: #ff5500;
margin-left: 10px;
}
.share-button:hover {
background-color: #ff3300;
border-color: #ff3300;
}
</style>
</head>
@ -157,7 +249,7 @@
<div class="container mt-5">
<h1 class="site-header"><a href="/">Raven Scott Music</a></h1>
<div class="search-container">
<input type="text" class="search-input" placeholder="Search tracks..." aria-label="Search tracks">
<input type="text" class="search-input" placeholder="Search tracks..." aria-label="Search tracks" autofocus>
<div class="search-results" id="search-results"></div>
</div>
<div class="card text-center">
@ -171,12 +263,24 @@
<p class="card-text">
<%- (track.description && track.description.trim()) ? track.description.replace(/\n/g, '<br>' )
: 'No description available for this track.' %>
<br>
<a id="back-button" class="btn btn-primary mt-3">Back</a>
</p>
<div class="mt-3">
<a id="back-button" class="btn btn-primary">Back</a>
<button class="btn share-button" id="share-button">Share</button>
</div>
</div>
</div>
</div>
<!-- <footer>
<p>© 2025 Raven Scott Music. All rights reserved.</p>
</footer> -->
<a href="#" class="back-to-top" id="back-to-top" title="Back to Top">
<svg width="24" height="24" fill="white" viewBox="0 0 24 24">
<path d="M12 2L3 11h6v10h6V11h6z"/>
</svg>
</a>
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.4/dist/umd/popper.min.js"></script>
@ -190,10 +294,30 @@
backButton.href = `/${genre}`;
console.log(`Set back button href to /${genre}`);
} else {
backButton.href = '/'; // Fallback to home page
backButton.href = '/';
console.warn('Genre not detected in URL, falling back to /');
}
// Share button functionality
const shareButton = document.getElementById('share-button');
shareButton.addEventListener('click', () => {
const shareData = {
title: `<%= track.title %>`,
text: `Check out this track by Raven Scott Music!`,
url: window.location.href
};
if (navigator.share) {
navigator.share(shareData)
.then(() => console.log('Track shared successfully'))
.catch(err => console.error('Error sharing track:', err));
} else {
// Fallback: Copy URL to clipboard
navigator.clipboard.writeText(window.location.href)
.then(() => alert('Track URL copied to clipboard!'))
.catch(err => console.error('Error copying URL:', err));
}
});
// Search functionality
const searchInput = document.querySelector('.search-input');
const searchResults = document.getElementById('search-results');
@ -217,10 +341,9 @@
}
}
// Initialize tracks on page load
fetchAllTracks();
// Debounce function to limit search frequency
// Debounce function
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
@ -253,6 +376,7 @@
filteredTracks.forEach(track => {
const resultItem = document.createElement('div');
resultItem.className = 'search-result-item';
resultItem.tabIndex = 0;
resultItem.innerHTML = `
${track.title}
<small>${track.genre.charAt(0).toUpperCase() + track.genre.slice(1)}</small>
@ -260,29 +384,84 @@
resultItem.addEventListener('click', () => {
window.location.href = `/${track.genre}/track/${track.slug}`;
});
resultItem.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
window.location.href = `/${track.genre}/track/${track.slug}`;
}
});
searchResults.appendChild(resultItem);
});
}
searchResults.classList.add('show');
updateSelectedResult();
}, 300);
// Search input event listener
searchInput.addEventListener('input', performSearch);
// Keyboard navigation for search results
function updateSelectedResult() {
const items = searchResults.querySelectorAll('.search-result-item');
items.forEach((item, index) => {
item.addEventListener('mouseover', () => {
items.forEach(i => i.classList.remove('selected'));
item.classList.add('selected');
selectedIndex = index;
});
});
}
// Hide search results when clicking outside
document.addEventListener('click', (e) => {
if (!searchContainer.contains(e.target)) {
searchResults.classList.remove('show');
let selectedIndex = -1;
searchInput.addEventListener('keydown', (e) => {
const items = searchResults.querySelectorAll('.search-result-item');
if (items.length === 0) return;
if (e.key === 'ArrowDown') {
e.preventDefault();
selectedIndex = Math.min(selectedIndex + 1, items.length - 1);
items.forEach(i => i.classList.remove('selected'));
items[selectedIndex].classList.add('selected');
items[selectedIndex].scrollIntoView({ block: 'nearest' });
} else if (e.key === 'ArrowUp') {
e.preventDefault();
selectedIndex = Math.max(selectedIndex - 1, -1);
items.forEach(i => i.classList.remove('selected'));
if (selectedIndex >= 0) {
items[selectedIndex].classList.add('selected');
items[selectedIndex].scrollIntoView({ block: 'nearest' });
}
} else if (e.key === 'Enter' && selectedIndex >= 0) {
e.preventDefault();
items[selectedIndex].click();
}
});
// Show search results when clicking input
searchInput.addEventListener('input', performSearch);
searchInput.addEventListener('focus', () => {
if (searchInput.value.trim().length >= 2) {
performSearch();
}
});
document.addEventListener('click', (e) => {
if (!searchContainer.contains(e.target)) {
searchResults.classList.remove('show');
selectedIndex = -1;
}
});
// Back to top button
const backToTop = document.getElementById('back-to-top');
window.addEventListener('scroll', () => {
if (window.scrollY > 300) {
backToTop.classList.add('show');
} else {
backToTop.classList.remove('show');
}
});
backToTop.addEventListener('click', (e) => {
e.preventDefault();
window.scrollTo({ top: 0, behavior: 'smooth' });
});
</script>
</body>