Add advanced search
This commit is contained in:
178
views/index.ejs
178
views/index.ejs
@ -130,18 +130,15 @@
|
|||||||
margin-top: -90px;
|
margin-top: -90px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Page container styling */
|
|
||||||
.page-container {
|
.page-container {
|
||||||
display: none;
|
display: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
/* Ensure full width for grid */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-container.active {
|
.page-container.active {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Iframe styling */
|
|
||||||
iframe {
|
iframe {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 166px;
|
height: 166px;
|
||||||
@ -150,7 +147,6 @@
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure row and columns work correctly */
|
|
||||||
.page-container .row {
|
.page-container .row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@ -161,41 +157,87 @@
|
|||||||
.pagination-btn {
|
.pagination-btn {
|
||||||
margin: 0 10px;
|
margin: 0 10px;
|
||||||
color: white;
|
color: white;
|
||||||
/* Ensure text is white for consistency */
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
/* Match the bold style of other UI elements */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hover state (already defined for btn-primary, but reinforcing for clarity) */
|
|
||||||
.pagination-btn.btn-primary:hover {
|
.pagination-btn.btn-primary:hover {
|
||||||
background-color: #ff3300;
|
background-color: #ff3300;
|
||||||
border-color: #ff3300;
|
border-color: #ff3300;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Disabled state */
|
|
||||||
.pagination-btn.btn-primary:disabled,
|
.pagination-btn.btn-primary:disabled,
|
||||||
.pagination-btn.btn-primary[disabled] {
|
.pagination-btn.btn-primary[disabled] {
|
||||||
background-color: #444;
|
background-color: #444;
|
||||||
/* Darker, muted gray to indicate inactivity */
|
|
||||||
border-color: #444;
|
border-color: #444;
|
||||||
color: #888;
|
color: #888;
|
||||||
/* Lighter gray text for readability */
|
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
/* Override Bootstrap's default opacity */
|
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
/* Clear visual cue for disabled state */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Active state (when button is clicked, if applicable) */
|
|
||||||
.pagination-btn.btn-primary:active,
|
.pagination-btn.btn-primary:active,
|
||||||
.pagination-btn.btn-primary:focus {
|
.pagination-btn.btn-primary:focus {
|
||||||
background-color: #cc4400;
|
background-color: #cc4400;
|
||||||
/* Slightly darker orange for feedback */
|
|
||||||
border-color: #cc4400;
|
border-color: #cc4400;
|
||||||
box-shadow: 0 0 8px rgba(255, 85, 0, 0.5);
|
box-shadow: 0 0 8px rgba(255, 85, 0, 0.5);
|
||||||
/* Subtle glow to match theme */
|
|
||||||
outline: none;
|
outline: none;
|
||||||
/* Remove default focus outline */
|
}
|
||||||
|
|
||||||
|
/* 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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@ -208,7 +250,7 @@
|
|||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<div class="collapse navbar-collapse" id="navbarNav">
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
<ul class="navbar-nav ml-auto">
|
<ul class="navbar-nav">
|
||||||
<% for (const genre in genreTracks) { %>
|
<% for (const genre in genreTracks) { %>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="#<%= genre %>">
|
<a class="nav-link" href="#<%= genre %>">
|
||||||
@ -217,6 +259,10 @@
|
|||||||
</li>
|
</li>
|
||||||
<% } %>
|
<% } %>
|
||||||
</ul>
|
</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>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@ -301,15 +347,12 @@
|
|||||||
console.log(`Received page_data for genre: ${genre}, page: ${page}, tracks: ${tracks.length}`);
|
console.log(`Received page_data for genre: ${genre}, page: ${page}, tracks: ${tracks.length}`);
|
||||||
const trackContainer = document.getElementById(`${genre}-tracks`);
|
const trackContainer = document.getElementById(`${genre}-tracks`);
|
||||||
|
|
||||||
// Initialize cache for genre
|
|
||||||
if (!pageCache[genre]) {
|
if (!pageCache[genre]) {
|
||||||
pageCache[genre] = {};
|
pageCache[genre] = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update current page
|
|
||||||
currentPages[genre] = page;
|
currentPages[genre] = page;
|
||||||
|
|
||||||
// Create new page if not cached
|
|
||||||
if (!pageCache[genre][page]) {
|
if (!pageCache[genre][page]) {
|
||||||
console.log(`Creating new page container for genre: ${genre}, page: ${page}`);
|
console.log(`Creating new page container for genre: ${genre}, page: ${page}`);
|
||||||
const pageContainer = document.createElement('div');
|
const pageContainer = document.createElement('div');
|
||||||
@ -317,11 +360,9 @@
|
|||||||
pageContainer.dataset.page = page;
|
pageContainer.dataset.page = page;
|
||||||
pageContainer.dataset.totalPages = totalPages;
|
pageContainer.dataset.totalPages = totalPages;
|
||||||
|
|
||||||
// Create row for grid
|
|
||||||
const row = document.createElement('div');
|
const row = document.createElement('div');
|
||||||
row.className = 'row';
|
row.className = 'row';
|
||||||
|
|
||||||
// Build track elements
|
|
||||||
tracks.forEach(track => {
|
tracks.forEach(track => {
|
||||||
const trackDiv = document.createElement('div');
|
const trackDiv = document.createElement('div');
|
||||||
trackDiv.className = 'col-md-6 mb-4';
|
trackDiv.className = 'col-md-6 mb-4';
|
||||||
@ -347,13 +388,11 @@
|
|||||||
lazyLoadIframes(pageContainer);
|
lazyLoadIframes(pageContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Switch to requested page
|
|
||||||
const allPages = trackContainer.querySelectorAll('.page-container');
|
const allPages = trackContainer.querySelectorAll('.page-container');
|
||||||
allPages.forEach(p => p.classList.remove('active'));
|
allPages.forEach(p => p.classList.remove('active'));
|
||||||
pageCache[genre][page].container.classList.add('active');
|
pageCache[genre][page].container.classList.add('active');
|
||||||
console.log(`Switched to page ${page} for genre: ${genre}`);
|
console.log(`Switched to page ${page} for genre: ${genre}`);
|
||||||
|
|
||||||
// Update pagination controls
|
|
||||||
const prevButton = document.getElementById(`${genre}-prev`);
|
const prevButton = document.getElementById(`${genre}-prev`);
|
||||||
const nextButton = document.getElementById(`${genre}-next`);
|
const nextButton = document.getElementById(`${genre}-next`);
|
||||||
prevButton.disabled = page === 1;
|
prevButton.disabled = page === 1;
|
||||||
@ -371,7 +410,6 @@
|
|||||||
console.error('WebSocket error:', message);
|
console.error('WebSocket error:', message);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Pagination button handlers
|
|
||||||
document.querySelectorAll('.pagination-btn').forEach(button => {
|
document.querySelectorAll('.pagination-btn').forEach(button => {
|
||||||
button.addEventListener('click', () => {
|
button.addEventListener('click', () => {
|
||||||
const genre = button.id.split('-')[0];
|
const genre = button.id.split('-')[0];
|
||||||
@ -386,7 +424,6 @@
|
|||||||
allPages.forEach(p => p.classList.remove('active'));
|
allPages.forEach(p => p.classList.remove('active'));
|
||||||
pageCache[genre][page].container.classList.add('active');
|
pageCache[genre][page].container.classList.add('active');
|
||||||
|
|
||||||
// Update pagination controls
|
|
||||||
const prevButton = document.getElementById(`${genre}-prev`);
|
const prevButton = document.getElementById(`${genre}-prev`);
|
||||||
const nextButton = document.getElementById(`${genre}-next`);
|
const nextButton = document.getElementById(`${genre}-next`);
|
||||||
const totalPages = parseInt(pageCache[genre][page].totalPages);
|
const totalPages = parseInt(pageCache[genre][page].totalPages);
|
||||||
@ -411,6 +448,95 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user