543 lines
20 KiB
Plaintext
543 lines
20 KiB
Plaintext
<!DOCTYPE html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Great Scott Music</title>
|
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
|
|
<style>
|
|
body {
|
|
background-color: black;
|
|
color: white;
|
|
font-family: 'Metal Mania', sans-serif;
|
|
}
|
|
|
|
.card {
|
|
background-color: #222;
|
|
border: 1px solid #444;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.card-body {
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.btn-primary {
|
|
background-color: #ff5500;
|
|
border-color: #ff5500;
|
|
transition: background-color 0.3s ease;
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
background-color: #ff3300;
|
|
}
|
|
|
|
.pagination-btn {
|
|
margin: 0 10px;
|
|
}
|
|
|
|
.navbar {
|
|
background-color: #1e1e1e;
|
|
padding: 1rem 2rem;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
|
border-bottom: 2px solid #ff5500;
|
|
}
|
|
|
|
.navbar-brand {
|
|
color: #ff5500 !important;
|
|
font-size: 1.8rem;
|
|
font-weight: bold;
|
|
text-transform: uppercase;
|
|
letter-spacing: 2px;
|
|
transition: color 0.3s ease;
|
|
}
|
|
|
|
.navbar-brand:hover {
|
|
color: #ff3300 !important;
|
|
}
|
|
|
|
.navbar-nav .nav-item {
|
|
margin: 0 1rem;
|
|
}
|
|
|
|
.nav-link {
|
|
color: white !important;
|
|
font-size: 1.2rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
padding: 0.5rem 1rem !important;
|
|
border-radius: 5px;
|
|
transition: all 0.3s ease;
|
|
position: relative;
|
|
}
|
|
|
|
.nav-link:hover {
|
|
color: #ff5500 !important;
|
|
background-color: rgba(255, 85, 0, 0.1);
|
|
}
|
|
|
|
.nav-link::after {
|
|
content: '';
|
|
position: absolute;
|
|
width: 0;
|
|
height: 2px;
|
|
bottom: 0;
|
|
left: 50%;
|
|
background-color: #ff5500;
|
|
transition: all 0.3s ease;
|
|
transform: translateX(-50%);
|
|
}
|
|
|
|
.nav-link:hover::after {
|
|
width: 80%;
|
|
}
|
|
|
|
.navbar-toggler {
|
|
border-color: #ff5500;
|
|
}
|
|
|
|
.navbar-toggler-icon {
|
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='%23ff5500' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
|
|
}
|
|
|
|
::-webkit-scrollbar {
|
|
width: 8px;
|
|
}
|
|
|
|
::-webkit-scrollbar-track {
|
|
background: #1e1e1e;
|
|
}
|
|
|
|
::-webkit-scrollbar-thumb {
|
|
background-color: #4a4a4a;
|
|
border-radius: 10px;
|
|
border: 2px solid #1e1e1e;
|
|
}
|
|
|
|
::-webkit-scrollbar-thumb:hover {
|
|
background-color: #555555;
|
|
}
|
|
|
|
* {
|
|
scrollbar-width: thin;
|
|
scrollbar-color: #4a4a4a #1e1e1e;
|
|
}
|
|
|
|
section {
|
|
padding-top: 90px;
|
|
margin-top: -90px;
|
|
}
|
|
|
|
.page-container {
|
|
display: none;
|
|
width: 100%;
|
|
}
|
|
|
|
.page-container.active {
|
|
display: block;
|
|
}
|
|
|
|
iframe {
|
|
width: 100%;
|
|
height: 166px;
|
|
max-width: 100%;
|
|
border: none;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.page-container .row {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
margin-right: -15px;
|
|
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%;
|
|
max-width: 300px;
|
|
}
|
|
|
|
.search-input {
|
|
background-color: #222;
|
|
border: 1px solid #444;
|
|
color: white;
|
|
border-radius: 4px;
|
|
padding: 8px 12px;
|
|
width: 100%;
|
|
transition: border-color 0.3s ease;
|
|
}
|
|
|
|
.search-input:focus {
|
|
border-color: #ff5500;
|
|
outline: none;
|
|
box-shadow: 0 0 5px rgba(255, 85, 0, 0.3);
|
|
}
|
|
|
|
.search-results {
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 0;
|
|
right: 0;
|
|
background-color: #222;
|
|
border: 1px solid #444;
|
|
border-radius: 4px;
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
z-index: 1000;
|
|
display: none;
|
|
margin-top: 5px;
|
|
}
|
|
|
|
.search-results.show {
|
|
display: block;
|
|
}
|
|
|
|
.search-result-item {
|
|
padding: 10px;
|
|
color: white;
|
|
cursor: pointer;
|
|
transition: background-color 0.2s ease;
|
|
}
|
|
|
|
.search-result-item:hover {
|
|
background-color: #ff5500;
|
|
}
|
|
|
|
.search-result-item small {
|
|
color: #aaa;
|
|
display: block;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<nav class="navbar navbar-expand-lg navbar-dark fixed-top">
|
|
<a class="navbar-brand" href="https://raven-scott.rocks">Raven Scott Music</a>
|
|
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav"
|
|
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
|
<span class="navbar-toggler-icon"></span>
|
|
</button>
|
|
<div class="collapse navbar-collapse" id="navbarNav">
|
|
<ul class="navbar-nav">
|
|
<% for (const genre in genreTracks) { %>
|
|
<li class="nav-item">
|
|
<a class="nav-link" href="#<%= genre %>">
|
|
<%= genre.charAt(0).toUpperCase() + genre.slice(1) %>
|
|
</a>
|
|
</li>
|
|
<% } %>
|
|
</ul>
|
|
<div class="search-container ml-auto">
|
|
<input type="text" class="search-input" placeholder="Search tracks or lyrics..." aria-label="Search tracks">
|
|
<div class="search-results" id="search-results"></div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="container mt-5 pt-5">
|
|
<% for (const genre in genreTracks) { %>
|
|
<section id="<%= genre %>">
|
|
<h2 class="text-center mb-4">
|
|
<%= genre.charAt(0).toUpperCase() + genre.slice(1) %> Tracks
|
|
</h2>
|
|
<div id="<%= genre %>-tracks">
|
|
<div class="page-container active" data-page="1"
|
|
data-total-pages="<%= genreTracks[genre].totalPages %>">
|
|
<div class="row">
|
|
<% genreTracks[genre].tracks.forEach(track=> { %>
|
|
<div class="col-md-6 mb-4" data-slug="<%= track.slug %>">
|
|
<div class="card text-center">
|
|
<div class="card-body">
|
|
<h5 class="card-title">
|
|
<%= track.title %>
|
|
</h5>
|
|
<p class="card-text"></p>
|
|
<iframe width="100%" height="166" scrolling="no" frameborder="no"
|
|
allow="autoplay"
|
|
data-src="https://w.soundcloud.com/player/?url=<%= track.url %>&color=%23ff5500&auto_play=false&show_artwork=true"
|
|
loading="lazy">
|
|
</iframe>
|
|
<a href="/<%= genre %>/track/<%= track.slug %>" class="btn btn-primary mt-3"
|
|
target="_blank">More Details</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% }) %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="text-center mb-4">
|
|
<button class="btn btn-primary pagination-btn" id="<%= genre %>-prev"
|
|
data-page="<%= genreTracks[genre].page - 1 %>" <%=genreTracks[genre].page===1 ? 'disabled' : ''
|
|
%>>Previous</button>
|
|
<span>Page <%= genreTracks[genre].page %> of <%= genreTracks[genre].totalPages %></span>
|
|
<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>
|
|
</section>
|
|
<% } %>
|
|
</div>
|
|
|
|
<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>
|
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
|
|
<script src="https://cdn.socket.io/4.5.0/socket.io.min.js"></script>
|
|
<script>
|
|
// 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) {
|
|
iframe.src = iframe.getAttribute('data-src');
|
|
console.log(`Lazy loading iframe for track: ${iframe.closest('[data-slug]').dataset.slug}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
window.addEventListener('load', () => lazyLoadIframes());
|
|
window.addEventListener('scroll', () => lazyLoadIframes());
|
|
|
|
// Page cache: { genre: { page: { container, totalPages } } }
|
|
const pageCache = {};
|
|
|
|
// Track current page per genre
|
|
const currentPages = {};
|
|
|
|
// WebSocket client
|
|
const socket = io();
|
|
|
|
socket.on('connect', () => {
|
|
console.log('Connected to WebSocket server');
|
|
});
|
|
|
|
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`);
|
|
|
|
if (!pageCache[genre]) {
|
|
pageCache[genre] = {};
|
|
}
|
|
|
|
currentPages[genre] = page;
|
|
|
|
if (!pageCache[genre][page]) {
|
|
console.log(`Creating new page container for genre: ${genre}, page: ${page}`);
|
|
const pageContainer = document.createElement('div');
|
|
pageContainer.className = 'page-container';
|
|
pageContainer.dataset.page = page;
|
|
pageContainer.dataset.totalPages = totalPages;
|
|
|
|
const row = document.createElement('div');
|
|
row.className = 'row';
|
|
|
|
tracks.forEach(track => {
|
|
const trackDiv = document.createElement('div');
|
|
trackDiv.className = 'col-md-6 mb-4';
|
|
trackDiv.dataset.slug = track.slug;
|
|
trackDiv.innerHTML = `
|
|
<div class="card text-center">
|
|
<div class="card-body">
|
|
<h5 class="card-title">${track.title}</h5>
|
|
<p class="card-text"></p>
|
|
<iframe width="100%" height="166" scrolling="no" frameborder="no" allow="autoplay"
|
|
data-src="https://w.soundcloud.com/player/?url=${track.url}&color=%23ff5500&auto_play=false&show_artwork=true"
|
|
loading="lazy"></iframe>
|
|
<a href="/${genre}/track/${track.slug}" class="btn btn-primary mt-3" target="_blank">More Details</a>
|
|
</div>
|
|
</div>
|
|
`;
|
|
row.appendChild(trackDiv);
|
|
});
|
|
|
|
pageContainer.appendChild(row);
|
|
trackContainer.appendChild(pageContainer);
|
|
pageCache[genre][page] = { container: pageContainer, totalPages };
|
|
lazyLoadIframes(pageContainer);
|
|
}
|
|
|
|
const allPages = trackContainer.querySelectorAll('.page-container');
|
|
allPages.forEach(p => p.classList.remove('active'));
|
|
pageCache[genre][page].container.classList.add('active');
|
|
console.log(`Switched to page ${page} for genre: ${genre}`);
|
|
|
|
const prevButton = document.getElementById(`${genre}-prev`);
|
|
const nextButton = document.getElementById(`${genre}-next`);
|
|
prevButton.disabled = page === 1;
|
|
nextButton.disabled = page === totalPages;
|
|
prevButton.dataset.page = page - 1;
|
|
nextButton.dataset.page = page + 1;
|
|
|
|
const pageInfo = document.querySelector(`#${genre}-tracks + .text-center span`);
|
|
pageInfo.textContent = `Page ${page} of ${totalPages}`;
|
|
|
|
lazyLoadIframes(pageCache[genre][page].container);
|
|
});
|
|
|
|
socket.on('error', ({ message }) => {
|
|
console.error('WebSocket error:', message);
|
|
});
|
|
|
|
document.querySelectorAll('.pagination-btn').forEach(button => {
|
|
button.addEventListener('click', () => {
|
|
const genre = button.id.split('-')[0];
|
|
const page = parseInt(button.dataset.page, 10);
|
|
console.log(`Button clicked: ${button.id}, genre: ${genre}, page: ${page}`);
|
|
|
|
if (page > 0 && page !== currentPages[genre]) {
|
|
if (pageCache[genre] && pageCache[genre][page]) {
|
|
console.log(`Loading cached page ${page} for genre: ${genre}`);
|
|
const trackContainer = document.getElementById(`${genre}-tracks`);
|
|
const allPages = trackContainer.querySelectorAll('.page-container');
|
|
allPages.forEach(p => p.classList.remove('active'));
|
|
pageCache[genre][page].container.classList.add('active');
|
|
|
|
const prevButton = document.getElementById(`${genre}-prev`);
|
|
const nextButton = document.getElementById(`${genre}-next`);
|
|
const totalPages = parseInt(pageCache[genre][page].totalPages);
|
|
prevButton.disabled = page === 1;
|
|
nextButton.disabled = page === totalPages;
|
|
prevButton.dataset.page = page - 1;
|
|
nextButton.dataset.page = page + 1;
|
|
|
|
const pageInfo = document.querySelector(`#${genre}-tracks + .text-center span`);
|
|
pageInfo.textContent = `Page ${page} of ${totalPages}`;
|
|
|
|
currentPages[genre] = page;
|
|
lazyLoadIframes(pageCache[genre][page].container);
|
|
} 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}`);
|
|
} else {
|
|
console.log(`Already on page ${page} for genre: ${genre}`);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Search functionality
|
|
const searchInput = document.querySelector('.search-input');
|
|
const searchResults = document.getElementById('search-results');
|
|
let allTracks = [];
|
|
const genres = ['metal', 'altrock', 'rap', 'lofi', 'edm', 'cuts'];
|
|
|
|
// Fetch all tracks from all genres
|
|
async function fetchAllTracks() {
|
|
try {
|
|
const fetchPromises = genres.map(genre =>
|
|
fetch(`/json/${genre}`)
|
|
.then(res => res.json())
|
|
.then(tracks => tracks.map(track => ({ ...track, genre })))
|
|
);
|
|
const tracksArrays = await Promise.all(fetchPromises);
|
|
allTracks = tracksArrays.flat();
|
|
console.log(`Fetched ${allTracks.length} tracks across all genres`);
|
|
} catch (err) {
|
|
console.error('Error fetching tracks:', err);
|
|
}
|
|
}
|
|
|
|
// Initialize tracks on page load
|
|
fetchAllTracks();
|
|
|
|
// Debounce function to limit search frequency
|
|
function debounce(func, wait) {
|
|
let timeout;
|
|
return function executedFunction(...args) {
|
|
const later = () => {
|
|
clearTimeout(timeout);
|
|
func(...args);
|
|
};
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(later, wait);
|
|
};
|
|
}
|
|
|
|
// Handle search
|
|
const performSearch = debounce(() => {
|
|
const query = searchInput.value.trim().toLowerCase();
|
|
searchResults.innerHTML = '';
|
|
if (query.length < 2) {
|
|
searchResults.classList.remove('show');
|
|
return;
|
|
}
|
|
|
|
const filteredTracks = allTracks.filter(track =>
|
|
track.title.toLowerCase().includes(query) ||
|
|
(track.description && track.description.toLowerCase().includes(query))
|
|
);
|
|
|
|
if (filteredTracks.length === 0) {
|
|
searchResults.innerHTML = '<div class="search-result-item">No results found</div>';
|
|
} else {
|
|
filteredTracks.forEach(track => {
|
|
const resultItem = document.createElement('div');
|
|
resultItem.className = 'search-result-item';
|
|
resultItem.innerHTML = `
|
|
${track.title}
|
|
<small>${track.genre.charAt(0).toUpperCase() + track.genre.slice(1)}</small>
|
|
`;
|
|
resultItem.addEventListener('click', () => {
|
|
window.location.href = `/${track.genre}/track/${track.slug}`;
|
|
});
|
|
searchResults.appendChild(resultItem);
|
|
});
|
|
}
|
|
|
|
searchResults.classList.add('show');
|
|
}, 300);
|
|
|
|
// Search input event listener
|
|
searchInput.addEventListener('input', performSearch);
|
|
|
|
// Hide search results when clicking outside
|
|
document.addEventListener('click', (e) => {
|
|
if (!searchContainer.contains(e.target)) {
|
|
searchResults.classList.remove('show');
|
|
}
|
|
});
|
|
|
|
// Show search results when clicking input
|
|
searchInput.addEventListener('focus', () => {
|
|
if (searchInput.value.trim().length >= 2) {
|
|
performSearch();
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
|
|
</html> |