'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 ? (

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 */}
);
}