diff --git a/views/index.ejs b/views/index.ejs
index 01a2be6..5dca3c1 100644
--- a/views/index.ejs
+++ b/views/index.ejs
@@ -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;
+ }
@@ -260,7 +351,7 @@
<% } %>
@@ -306,10 +397,21 @@
+
<% } %>
+
+
+
+
+
@@ -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) {
- iframe.src = iframe.getAttribute('data-src');
- console.log(`Lazy loading iframe for track: ${iframe.closest('[data-slug]').dataset.slug}`);
- }
- });
+ 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}
${track.genre.charAt(0).toUpperCase() + track.genre.slice(1)}
@@ -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' });
+ });