'use client'; import { useEffect, useState, useRef, useCallback } from 'react'; import { useSearchParams } from 'next/navigation'; import Link from 'next/link'; import { searchVideosClient, getTrendingVideosClient } from './clientActions'; import { VideoData } from './constants'; import LoadingSpinner from './components/LoadingSpinner'; // Format view count function formatViews(views: number): string { if (views >= 1000000) return (views / 1000000).toFixed(1) + 'M views'; if (views >= 1000) return (views / 1000).toFixed(0) + 'K views'; return views === 0 ? '' : `${views} views`; } // Get stable time ago based on video ID (deterministic, not random) function getStableTimeAgo(videoId: string): string { const times = ['2 hours ago', '5 hours ago', '1 day ago', '2 days ago', '3 days ago', '1 week ago', '2 weeks ago', '1 month ago']; const hash = videoId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0); return times[hash % times.length]; } // Get fallback thumbnail URL (always works) function getFallbackThumbnail(videoId: string): string { return `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`; } // Video Card Component function VideoCard({ video }: { video: VideoData }) { const [imgError, setImgError] = useState(false); const [imgLoaded, setImgLoaded] = useState(false); // Use multiple thumbnail sources for fallback const thumbnailSources = [ `https://i.ytimg.com/vi/${video.id}/hqdefault.jpg`, `https://i.ytimg.com/vi/${video.id}/mqdefault.jpg`, `https://i.ytimg.com/vi/${video.id}/sddefault.jpg`, `https://i.ytimg.com/vi/${video.id}/default.jpg`, ]; const [currentSrcIndex, setCurrentSrcIndex] = useState(0); const currentSrc = thumbnailSources[currentSrcIndex]; const handleError = () => { if (currentSrcIndex < thumbnailSources.length - 1) { setCurrentSrcIndex(prev => prev + 1); } else { setImgError(true); } }; return (
{/* Thumbnail */}
{!imgLoaded && !imgError && (
)} {!imgError ? ( {video.title} setImgLoaded(true)} style={{ width: '100%', height: '100%', objectFit: 'cover', display: imgLoaded ? 'block' : 'none', transition: 'opacity 0.2s', }} /> ) : (
)} {/* Duration badge */} {video.duration && (
{video.duration}
)} {/* Hover overlay */}
{/* Video Info */}
{/* Title - max 2 lines */}

{video.title}

{/* Channel name */}
{video.uploader}
{/* Views and time */}
{(video.view_count ?? 0) > 0 && {formatViews(video.view_count ?? 0)}} {(video.view_count ?? 0) > 0 && } {video.upload_date || video.publishedAt || getStableTimeAgo(video.id)}
); } // Category Pills Component function CategoryPills({ categories, currentCategory, onCategoryChange }: { categories: string[]; currentCategory: string; onCategoryChange: (category: string) => void; }) { return (
{categories.map((category) => ( ))}
); } // Loading Skeleton function VideoSkeleton() { return (
); } // Get region from cookie function getRegionFromCookie(): string { if (typeof document === 'undefined') return 'VN'; const match = document.cookie.match(/(?:^|; )region=([^;]*)/); return match ? decodeURIComponent(match[1]) : 'VN'; } // Check if thumbnail URL is valid (not a 404 placeholder) function isValidThumbnail(thumbnail: string | undefined): boolean { if (!thumbnail) return false; // YouTube default thumbnails that are usually available const validPatterns = [ 'i.ytimg.com/vi/', 'i.ytimg.com/vi_webp/', ]; return validPatterns.some(pattern => thumbnail.includes(pattern)); } export default function ClientHomePage() { const searchParams = useSearchParams(); const categoryParam = searchParams.get('category') || 'All'; const [videos, setVideos] = useState([]); const [loading, setLoading] = useState(true); const [loadingMore, setLoadingMore] = useState(false); const [currentCategory, setCurrentCategory] = useState(categoryParam); const [page, setPage] = useState(1); const [regionCode, setRegionCode] = useState('VN'); const [hasMore, setHasMore] = useState(true); // Use refs to track state for the observer callback const loadingMoreRef = useRef(false); const loadingRef = useRef(true); const hasMoreRef = useRef(true); const pageRef = useRef(1); useEffect(() => { loadingMoreRef.current = loadingMore; }, [loadingMore]); useEffect(() => { loadingRef.current = loading; }, [loading]); useEffect(() => { hasMoreRef.current = hasMore; }, [hasMore]); useEffect(() => { pageRef.current = page; }, [page]); const categories = ['All', 'Trending', 'Music', 'Gaming', 'News', 'Sports', 'Live', 'New']; // Region mapping for YouTube API const REGION_MAP: Record = { 'VN': 'Vietnam', 'US': 'United States', 'JP': 'Japan', 'KR': 'South Korea', 'IN': 'India', 'GB': 'United Kingdom', 'GLOBAL': '', }; // Initialize region from cookie useEffect(() => { const region = getRegionFromCookie(); setRegionCode(region); }, []); // Load videos when category or region changes useEffect(() => { loadVideos(currentCategory, 1); }, [currentCategory, regionCode]); // Listen for region changes useEffect(() => { const checkRegionChange = () => { const newRegion = getRegionFromCookie(); setRegionCode(prev => { if (newRegion !== prev) { return newRegion; } return prev; }); }; // Listen for custom event from RegionSelector const handleRegionChange = (e: CustomEvent) => { if (e.detail?.region) { setRegionCode(e.detail.region); } }; // Check when tab becomes visible const handleVisibilityChange = () => { if (document.visibilityState === 'visible') { checkRegionChange(); } }; // Check when window gets focus const handleFocus = () => { checkRegionChange(); }; window.addEventListener('regionchange', handleRegionChange as EventListener); document.addEventListener('visibilitychange', handleVisibilityChange); window.addEventListener('focus', handleFocus); // Also poll every 3 seconds as backup const interval = setInterval(checkRegionChange, 3000); return () => { window.removeEventListener('regionchange', handleRegionChange as EventListener); document.removeEventListener('visibilitychange', handleVisibilityChange); window.removeEventListener('focus', handleFocus); clearInterval(interval); }; }, []); // Run once on mount const loadVideos = async (category: string, pageNum: number) => { try { setLoading(true); let results: VideoData[] = []; const regionLabel = REGION_MAP[regionCode] || ''; const regionSuffix = regionLabel ? ` ${regionLabel}` : ''; // All categories use region-specific search if (category === 'Trending') { results = await getTrendingVideosClient(regionCode, 30); } else if (category === 'All') { // Use region-specific trending for "All" results = await getTrendingVideosClient(regionCode, 30); } else { // Category-specific search with region const query = `${category}${regionSuffix}`; results = await searchVideosClient(query, 30); } // Remove duplicates and filter out videos without thumbnails const uniqueResults = results.filter((video, index, self) => { const isUnique = index === self.findIndex(v => v.id === video.id); const hasThumbnail = isValidThumbnail(video.thumbnail); return isUnique && hasThumbnail; }); setVideos(uniqueResults); setPage(pageNum); setHasMore(true); hasMoreRef.current = true; } catch (error) { console.error('Failed to load videos:', error); } finally { setLoading(false); } }; const handleCategoryChange = (category: string) => { setCurrentCategory(category); const url = new URL(window.location.href); url.searchParams.set('category', category); window.history.pushState({}, '', url); }; const loadMore = useCallback(async () => { if (loadingMoreRef.current || loadingRef.current || !hasMoreRef.current) return; setLoadingMore(true); const nextPage = pageRef.current + 1; try { const regionLabel = REGION_MAP[regionCode] || ''; const regionSuffix = regionLabel ? ` ${regionLabel}` : ''; // Generate varied search queries - ALL include region const searchVariations = [ `trending${regionSuffix}`, `popular videos${regionSuffix}`, `viral 2026${regionSuffix}`, `music${regionSuffix}`, `entertainment${regionSuffix}`, `gaming${regionSuffix}`, `funny${regionSuffix}`, `news${regionSuffix}`, `sports${regionSuffix}`, `new videos${regionSuffix}`, ]; const queryIndex = (nextPage - 1) % searchVariations.length; const searchQuery = searchVariations[queryIndex]; // Always use search for variety - trending API returns same results const moreVideos = await searchVideosClient(searchQuery, 30); // Remove duplicates and filter out videos without thumbnails setVideos(prev => { const existingIds = new Set(prev.map(v => v.id)); const uniqueNewVideos = moreVideos.filter(v => !existingIds.has(v.id) && isValidThumbnail(v.thumbnail) ); // If no new videos after filtering, stop infinite scroll if (uniqueNewVideos.length < 3) { setHasMore(false); hasMoreRef.current = false; } return [...prev, ...uniqueNewVideos]; }); setPage(nextPage); } catch (error) { console.error('Failed to load more videos:', error); // Don't stop infinite scroll on error - allow retry on next scroll } finally { setLoadingMore(false); } }, [currentCategory, regionCode]); // Ref for the loadMore function to avoid stale closures const loadMoreCallbackRef = useRef(loadMore); useEffect(() => { loadMoreCallbackRef.current = loadMore; }, [loadMore]); // Infinite scroll using Intersection Observer useEffect(() => { // Don't set up observer while loading or if no videos if (loading || videos.length === 0) return; const observer = new IntersectionObserver( (entries) => { const entry = entries[0]; if (entry.isIntersecting && !loadingMoreRef.current && !loadingRef.current && hasMoreRef.current) { console.log('Sentinel intersecting, loading more...'); loadMoreCallbackRef.current(); } }, { rootMargin: '600px', threshold: 0 } ); // Small delay to ensure DOM is ready const timer = setTimeout(() => { const sentinel = document.getElementById('scroll-sentinel'); console.log('Sentinel element:', sentinel); if (sentinel) { observer.observe(sentinel); } }, 50); return () => { clearTimeout(timer); observer.disconnect(); }; }, [loading, videos.length]); // Re-run when loading finishes or videos change return (
{/* Category Pills */} {/* Video Grid */} {loading ? (
{[...Array(12)].map((_, i) => ( ))}
) : ( <>
{videos.map((video) => ( ))}
{/* Scroll Sentinel for Infinite Scroll */}
{/* Loading More Indicator */} {loadingMore && (
)} {/* End of Results */} {!hasMore && videos.length > 0 && (
You've reached the end
)} {/* Empty State */} {videos.length === 0 && !loading && (

No videos found

Try selecting a different category

)} )}
{/* Animations */}
); }