Add search box to track pages
This commit is contained in:
181
views/track.ejs
181
views/track.ejs
@ -18,52 +18,120 @@
|
||||
.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;
|
||||
}
|
||||
|
||||
/* Custom slim dark mode scrollbar for webkit browsers (Chrome, Safari, Edge) */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
/* Slim width for the scrollbar */
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #1e1e1e;
|
||||
/* Dark background for the scrollbar track */
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #4a4a4a;
|
||||
/* Slightly lighter thumb color */
|
||||
border-radius: 10px;
|
||||
/* Rounded corners for the scrollbar */
|
||||
border: 2px solid #1e1e1e;
|
||||
/* Matches the track background to create a gap effect */
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #555555;
|
||||
/* Lighter on hover */
|
||||
}
|
||||
|
||||
/* Scrollbar styling for Firefox */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #4a4a4a #1e1e1e;
|
||||
}
|
||||
|
||||
/* Search box styling */
|
||||
.search-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
margin: 0 auto 20px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
/* Adjust container padding */
|
||||
.container {
|
||||
padding-top: 30px;
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container mt-5">
|
||||
<div class="search-container">
|
||||
<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 class="card text-center">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title">
|
||||
@ -75,12 +143,107 @@
|
||||
<p class="card-text">
|
||||
<%- (track.description && track.description.trim()) ? track.description.replace(/\n/g, '<br>' )
|
||||
: 'No description available for this track.' %>
|
||||
<BR>
|
||||
<br>
|
||||
<a href="/" class="btn btn-primary mt-3">Back</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
// 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'];
|
||||
|
||||
// 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>
|
Reference in New Issue
Block a user