// KV-Tube Main JavaScript - YouTube Clone document.addEventListener('DOMContentLoaded', () => { const searchInput = document.getElementById('searchInput'); const resultsArea = document.getElementById('resultsArea'); if (searchInput) { searchInput.addEventListener('keypress', async (e) => { if (e.key === 'Enter') { e.preventDefault(); const query = searchInput.value.trim(); if (query) { // Check if on search page already, if not redirect // Since we are SPA-ish, we just call searchYouTube // But if we want a dedicated search page URL, we could do: // window.history.pushState({}, '', `/?q=${encodeURIComponent(query)}`); searchYouTube(query); } } }); // Load trending on init loadTrending(); } // Init Theme initTheme(); }); // Note: Global variables like currentCategory are defined below let currentCategory = 'general'; let currentPage = 1; let isLoading = false; // --- UI Helpers --- function renderSkeleton() { // Generate 8 skeleton cards return Array(8).fill(0).map(() => `
`).join(''); } function renderNoContent(message = 'Try searching for something else', title = 'No videos found') { return `
${title}
${message}
`; } // Search YouTube videos async function searchYouTube(query) { if (isLoading) return; const resultsArea = document.getElementById('resultsArea'); const loadMoreArea = document.getElementById('loadMoreArea'); isLoading = true; resultsArea.innerHTML = renderSkeleton(); try { const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`); const data = await response.json(); if (data.error) { resultsArea.innerHTML = `

Error: ${data.error}

`; return; } displayResults(data, false); if (loadMoreArea) loadMoreArea.style.display = 'none'; } catch (error) { console.error('Search error:', error); resultsArea.innerHTML = `

Failed to fetch results

`; } finally { isLoading = false; } } // Switch category async function switchCategory(category, btn) { if (isLoading) return; // Update UI (Pills) document.querySelectorAll('.yt-category-pill').forEach(b => b.classList.remove('active')); if (btn && btn.classList) btn.classList.add('active'); // Update UI (Sidebar) document.querySelectorAll('.yt-sidebar-item').forEach(item => { item.classList.remove('active'); if (item.getAttribute('data-category') === category) { item.classList.add('active'); } }); // Reset state currentCategory = category; currentPage = 1; window.currentPage = 1; const resultsArea = document.getElementById('resultsArea'); resultsArea.innerHTML = renderSkeleton(); // Hide pagination while loading const paginationArea = document.getElementById('paginationArea'); if (paginationArea) paginationArea.style.display = 'none'; // Load both videos and shorts with current category, sort, and region await loadTrending(true); // Also reload shorts to match category if (typeof loadShorts === 'function') { loadShorts(); } // Render pagination if (typeof renderPagination === 'function') { renderPagination(); } } // Load more videos async function loadMore() { currentPage++; await loadTrending(false); } // Load trending videos async function loadTrending(reset = true) { if (isLoading && reset) isLoading = false; const resultsArea = document.getElementById('resultsArea'); const loadMoreArea = document.getElementById('loadMoreArea'); const loadMoreBtn = document.getElementById('loadMoreBtn'); if (!resultsArea) return; // Exit if not on home page isLoading = true; if (!reset && loadMoreBtn) { loadMoreBtn.innerHTML = ' Loading...'; } try { // Get sort and region values from page (if available) const sortValue = window.currentSort || 'month'; const regionValue = window.currentRegion || 'vietnam'; const response = await fetch(`/api/trending?category=${currentCategory}&page=${currentPage}&sort=${sortValue}®ion=${regionValue}`); const data = await response.json(); if (data.error) { console.error('Trending error:', data.error); if (reset) { resultsArea.innerHTML = renderNoContent(`Error: ${data.error}`, 'Something went wrong'); } return; } if (reset) resultsArea.innerHTML = ''; if (data.length === 0) { if (reset) { resultsArea.innerHTML = renderNoContent(); } const paginationArea = document.getElementById('paginationArea'); if (paginationArea) paginationArea.style.display = 'none'; } else { displayResults(data, !reset); const paginationArea = document.getElementById('paginationArea'); if (paginationArea) paginationArea.style.display = 'flex'; // Update pagination if function exists if (typeof renderPagination === 'function') { renderPagination(); } } } catch (e) { console.error('Failed to load trending:', e); if (reset) { resultsArea.innerHTML = `

Connection error

`; } } finally { isLoading = false; } } // Display results with YouTube-style cards function displayResults(videos, append = false) { const resultsArea = document.getElementById('resultsArea'); if (!append) resultsArea.innerHTML = ''; if (videos.length === 0 && !append) { resultsArea.innerHTML = renderNoContent(); return; } videos.forEach(video => { const card = document.createElement('div'); card.className = 'yt-video-card'; card.innerHTML = `
${escapeHtml(video.title)} ${video.duration ? `${video.duration}` : ''}
${video.uploader ? video.uploader.charAt(0).toUpperCase() : 'Y'}

${escapeHtml(video.title)}

${escapeHtml(video.uploader || 'Unknown')}

${formatViews(video.view_count)} views • ${formatDate(video.upload_date)}

`; card.addEventListener('click', () => { window.location.href = `/watch?v=${video.id}`; }); resultsArea.appendChild(card); }); } // Format view count (YouTube style) function formatViews(views) { if (!views) return '0'; const num = parseInt(views); if (num >= 1000000000) return (num / 1000000000).toFixed(1) + 'B'; if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M'; if (num >= 1000) return (num / 1000).toFixed(1) + 'K'; return num.toLocaleString(); } // Format date (YouTube style: "2 hours ago", "3 days ago", etc.) function formatDate(dateStr) { if (!dateStr) return 'Recently'; // Handle YYYYMMDD format if (/^\d{8}$/.test(dateStr)) { const year = dateStr.substring(0, 4); const month = dateStr.substring(4, 6); const day = dateStr.substring(6, 8); dateStr = `${year}-${month}-${day}`; } const date = new Date(dateStr); if (isNaN(date.getTime())) return 'Recently'; const now = new Date(); const diffMs = now - date; const diffSec = Math.floor(diffMs / 1000); const diffMin = Math.floor(diffSec / 60); const diffHour = Math.floor(diffMin / 60); const diffDay = Math.floor(diffHour / 24); const diffWeek = Math.floor(diffDay / 7); const diffMonth = Math.floor(diffDay / 30); const diffYear = Math.floor(diffDay / 365); if (diffYear > 0) return `${diffYear} year${diffYear > 1 ? 's' : ''} ago`; if (diffMonth > 0) return `${diffMonth} month${diffMonth > 1 ? 's' : ''} ago`; if (diffWeek > 0) return `${diffWeek} week${diffWeek > 1 ? 's' : ''} ago`; if (diffDay > 0) return `${diffDay} day${diffDay > 1 ? 's' : ''} ago`; if (diffHour > 0) return `${diffHour} hour${diffHour > 1 ? 's' : ''} ago`; if (diffMin > 0) return `${diffMin} minute${diffMin > 1 ? 's' : ''} ago`; return 'Just now'; } // Escape HTML to prevent XSS function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Sidebar toggle (for mobile) function toggleSidebar() { const sidebar = document.getElementById('sidebar'); const main = document.getElementById('mainContent'); if (window.innerWidth <= 1024) { sidebar.classList.toggle('open'); } else { sidebar.classList.toggle('collapsed'); main.classList.toggle('sidebar-collapsed'); localStorage.setItem('sidebarCollapsed', sidebar.classList.contains('collapsed')); } } // Close sidebar when clicking outside (mobile) document.addEventListener('click', (e) => { const sidebar = document.getElementById('sidebar'); const menuBtn = document.querySelector('.yt-menu-btn'); if (window.innerWidth <= 1024 && sidebar && sidebar.classList.contains('open') && !sidebar.contains(e.target) && menuBtn && !menuBtn.contains(e.target)) { sidebar.classList.remove('open'); } }); // --- Theme Logic --- function initTheme() { const savedTheme = localStorage.getItem('theme') || 'dark'; document.documentElement.setAttribute('data-theme', savedTheme); // Update toggle if exists const toggle = document.getElementById('themeToggle'); if (toggle) { toggle.checked = savedTheme === 'dark'; } } function toggleTheme() { const current = document.documentElement.getAttribute('data-theme'); const newTheme = current === 'light' ? 'dark' : 'light'; document.documentElement.setAttribute('data-theme', newTheme); localStorage.setItem('theme', newTheme); } // --- Profile Logic --- async function updateProfile(e) { if (e) e.preventDefault(); const displayName = document.getElementById('displayName').value; const btn = e.target.querySelector('button'); const originalText = btn.innerHTML; btn.disabled = true; btn.innerHTML = ' Saving...'; try { const response = await fetch('/api/update_profile', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ username: displayName }) }); const data = await response.json(); if (data.success) { showToast('Profile updated successfully!', 'success'); // Update UI immediately const avatarName = document.querySelector('.yt-avatar'); if (avatarName) avatarName.title = displayName; } else { showToast(data.message || 'Update failed', 'error'); } } catch (err) { showToast('Network error', 'error'); } finally { btn.disabled = false; btn.innerHTML = originalText; } }