lukawskikacper's picture
Fix global search
0ad6b1b
// Common functionality
// Initialize on page load
document.addEventListener('DOMContentLoaded', () => {
// Display recent videos in the footer on page load
loadFooterRecentVideos();
// Handle theme switching
const themeItems = document.querySelectorAll('.theme-item');
themeItems.forEach(item => {
item.addEventListener('click', () => {
const theme = item.dataset.theme;
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
});
});
// Apply saved theme from localStorage if available
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
document.documentElement.setAttribute('data-theme', savedTheme);
}
// Handle global search
const searchButton = document.getElementById('global-search-button');
const searchInput = document.getElementById('global-search');
if (searchButton && searchInput) {
searchButton.addEventListener('click', () => {
handleGlobalSearch();
});
searchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
handleGlobalSearch();
}
});
}
});
// Format seconds to HH:MM:SS format
function formatTime(seconds) {
const hours = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
if (hours > 0) {
return `${hours}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
} else {
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
}
// Error handling function
function handleError(error) {
console.error('Error:', error);
return `<div role="alert" class="alert alert-error">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>Error: ${error.message || 'Something went wrong'}</span>
<div>
<button class="btn btn-sm btn-ghost" onclick="window.location.reload()">Retry</button>
</div>
</div>`;
}
// Toast notification function
function showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `alert alert-${type} fixed bottom-4 right-4 max-w-xs z-50 shadow-lg`;
// Different icon based on type
let icon = '';
switch(type) {
case 'success':
icon = `<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>`;
break;
case 'warning':
icon = `<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>`;
break;
case 'error':
icon = `<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>`;
break;
default: // info
icon = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>`;
}
toast.innerHTML = `
${icon}
<span>${message}</span>
<div>
<button class="btn btn-sm btn-ghost" onclick="this.parentElement.parentElement.remove()">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
`;
document.body.appendChild(toast);
// Auto-dismiss after 3 seconds
setTimeout(() => {
toast.classList.add('opacity-0', 'transition-opacity', 'duration-500');
setTimeout(() => toast.remove(), 500);
}, 3000);
}
// Extract video ID from YouTube URL
function extractVideoId(url) {
const regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/;
const match = url.match(regExp);
return (match && match[7].length === 11) ? match[7] : null;
}
// Handle global search
function handleGlobalSearch() {
const searchInput = document.getElementById('global-search');
const searchTerm = searchInput.value.trim();
if (searchTerm) {
// Get modal elements
const searchModal = document.getElementById('search-results-modal');
const searchResultsContainer = document.getElementById('search-results-container');
const modalTitle = searchModal.querySelector('h3');
// Update modal title with search term
modalTitle.textContent = `Search Results for "${searchTerm}"`;
// Show the modal
searchModal.showModal();
// Clear previous results and show loading state
searchResultsContainer.innerHTML = `
<div class="flex justify-center items-center p-4">
<span class="loading loading-spinner loading-md"></span>
<span class="ml-2">Searching...</span>
</div>
`;
// Fetch search results from API
fetch(`/api/video/search?query=${encodeURIComponent(searchTerm)}&limit=10`)
.then(response => {
if (!response.ok) {
throw new Error('Failed to perform search');
}
return response.json();
})
.then(results => {
if (results && results.length > 0) {
// Group results by video
const videoGroups = {};
// First pass: collect all video IDs that need titles
const videoIds = [];
results.forEach(result => {
const videoId = result.segment.video_id;
if (!videoGroups[videoId]) {
videoGroups[videoId] = {
videoId: videoId,
title: `Video ${videoId}`, // Default title, will be updated
segments: []
};
// Add to list of IDs to fetch titles for
videoIds.push(videoId);
}
videoGroups[videoId].segments.push(result);
});
// If we have video IDs, fetch their proper titles
const videoPromises = videoIds.map(videoId => {
return fetch(`/api/video/info/${videoId}`)
.then(response => response.ok ? response.json() : null)
.then(videoInfo => {
if (videoInfo && videoInfo.title && videoGroups[videoId]) {
videoGroups[videoId].title = videoInfo.title;
}
})
.catch(error => console.error(`Error fetching video info for ${videoId}:`, error));
});
// After all video titles are fetched, continue with rendering
Promise.all(videoPromises).then(() => {
// Generate results HTML
const resultsHTML = Object.values(videoGroups).map(group => {
const segmentsHTML = group.segments.map(result => {
return `
<a href="/video/${group.videoId}?t=${Math.floor(result.segment.start)}"
class="block p-2 hover:bg-base-200 rounded-md transition-colors duration-150 mb-2">
<div class="flex items-start">
<div class="text-primary font-mono mr-2">${formatTime(result.segment.start)}</div>
<div class="flex-grow">
<p class="truncate-3-lines">${result.segment.text}</p>
<div class="text-xs opacity-70 mt-1">Score: ${(result.score * 100).toFixed(1)}%</div>
</div>
</div>
</a>
`;
}).join('');
return `
<div class="mb-6">
<div class="flex items-center mb-2">
<img src="https://img.youtube.com/vi/${group.videoId}/default.jpg" alt="Thumbnail"
class="w-12 h-12 rounded-md mr-2">
<h3 class="text-lg font-bold">
<a href="/video/${group.videoId}" class="link">${group.title}</a>
</h3>
</div>
<div class="pl-4 border-l-2 border-primary">
${segmentsHTML}
</div>
</div>
`;
}).join('');
// Update search results container
searchResultsContainer.innerHTML = resultsHTML;
});
} else {
// No results found - display immediately, no need to wait for titles
searchResultsContainer.innerHTML = `
<div class="alert alert-info">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>No results found. Try a different search term or process more videos.</span>
</div>
`;
}
})
.catch(error => {
console.error('Search error:', error);
searchResultsContainer.innerHTML = `
<div class="alert alert-error">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>Error performing search: ${error.message}</span>
</div>
`;
});
} else {
// Show notification if search is empty
showToast('Please enter a search term', 'warning');
}
}
// Load recent videos into the footer from the API
function loadFooterRecentVideos() {
const footerRecentVideos = document.getElementById('footer-recent-videos');
if (!footerRecentVideos) return;
// Show loading state
footerRecentVideos.innerHTML = '<p class="text-sm opacity-70">Loading recent videos...</p>';
// Fetch recent videos from server API
fetch('/api/video/recent?limit=3')
.then(response => {
if (!response.ok) {
throw new Error('Failed to fetch recent videos');
}
return response.json();
})
.then(videos => {
if (videos && videos.length > 0) {
// Generate HTML for recent videos
const videoLinks = videos.map(video => {
return `
<a href="/video/${video.video_id}" class="link link-hover block py-1 truncate">
<span class="text-xs text-primary">▶</span> ${video.title || `Video ${video.video_id}`}
</a>
`;
}).join('');
// Add videos to the footer
footerRecentVideos.innerHTML = videoLinks;
} else {
footerRecentVideos.innerHTML = '<p class="text-sm opacity-70">No recent videos</p>';
}
})
.catch(error => {
console.error('Error loading footer videos:', error);
footerRecentVideos.innerHTML = '<p class="text-sm opacity-70">Failed to load recent videos</p>';
});
}