Files
ravenscott-rocks/views/index.ejs
snxraven b12b3882f3 Add pagination and WebSocket support for track listings
This commit introduces pagination and real-time updates to the music site, improving performance and user experience for browsing tracks across genres. Key changes include:

music_site.js:

Added Socket.IO for real-time communication between client and server.

Implemented pagination with TRACKS_PER_PAGE (4 tracks per page) in getTracks function, returning paginated tracks, current page, total pages, and total tracks.

Enhanced slug generation with usedSlugs Map to ensure uniqueness within genres.

Updated routes and WebSocket handlers to support pagination:

Modified getTracks to handle page parameters and return paginated data.

Added io.on('connection') to handle request_page events and emit page_data or error.

Updated / and /sitemap.xml to work with paginated track data.

Adjusted track retrieval in /:genre/track/:slug and /json/:genre to use allTracks for consistency.

index.ejs:

Added pagination controls (Previous/Next buttons) for each genre section, displaying current page and total pages.

Introduced page-container divs to manage paginated track displays, with active class for the current page.

Implemented client-side WebSocket logic with Socket.IO to request and render new pages dynamically.

Added page caching (pageCache) to store rendered pages and reduce server requests.

Enhanced lazy loading of iframes to apply to dynamically loaded pages.

Updated styling for page-container and iframes to ensure proper layout and responsiveness.

These changes enable users to navigate through tracks efficiently without loading all tracks at once, reduce server load, and provide a smoother browsing experience with real-time updates.
2025-06-22 02:54:41 -04:00

368 lines
14 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 styling */
.page-container {
display: none;
width: 100%; /* Ensure full width for grid */
}
.page-container.active {
display: block;
}
/* Iframe styling */
iframe {
width: 100%;
height: 166px;
max-width: 100%;
border: none;
border-radius: 4px;
}
/* Ensure row and columns work correctly */
.page-container .row {
display: flex;
flex-wrap: wrap;
margin-right: -15px;
margin-left: -15px;
}
</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 ml-auto">
<% 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>
</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`);
// Initialize cache for genre
if (!pageCache[genre]) {
pageCache[genre] = {};
}
// Update current page
currentPages[genre] = page;
// Create new page if not cached
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;
// Create row for grid
const row = document.createElement('div');
row.className = 'row';
// Build track elements
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">More Details</a>
</div>
</div>
`;
row.appendChild(trackDiv);
});
pageContainer.appendChild(row);
trackContainer.appendChild(pageContainer);
pageCache[genre][page] = { container: pageContainer, totalPages };
lazyLoadIframes(pageContainer);
}
// Switch to requested page
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}`);
// Update pagination controls
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);
});
// Pagination button handlers
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');
// Update pagination controls
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}`);
}
});
});
</script>
</body>
</html>