'use client'; import { useEffect, useState, useCallback } from 'react'; import { useSearchParams, useRouter } from 'next/navigation'; import YouTubePlayer from './YouTubePlayer'; import { getVideoDetailsClient, getRelatedVideosClient, getCommentsClient, searchVideosClient } from '../clientActions'; import { VideoData } from '../constants'; import { isSubscribed, toggleSubscription, addToHistory, isVideoSaved, toggleSaveVideo } from '../storage'; import LoadingSpinner from '../components/LoadingSpinner'; import Link from 'next/link'; // Simple cache for API responses to reduce quota usage const apiCache = new Map(); const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes function getCachedData(key: string) { const cached = apiCache.get(key); if (cached && Date.now() - cached.timestamp < CACHE_DURATION) { return cached.data; } return null; } function setCachedData(key: string, data: any) { apiCache.set(key, { data, timestamp: Date.now() }); // Clean up old cache entries if (apiCache.size > 100) { const oldestKey = apiCache.keys().next().value; if (oldestKey) { apiCache.delete(oldestKey); } } } // Video Info Section function VideoInfo({ video }: { video: any }) { const [expanded, setExpanded] = useState(false); const [subscribed, setSubscribed] = useState(false); const [isSaved, setIsSaved] = useState(false); const [subscribing, setSubscribing] = useState(false); // Check subscription and save status on mount useEffect(() => { if (video?.channelId) { setSubscribed(isSubscribed(video.channelId)); } if (video?.id) { setIsSaved(isVideoSaved(video.id)); } }, [video?.channelId, video?.id]); const handleSubscribe = useCallback(() => { if (!video?.channelId || subscribing) return; setSubscribing(true); try { const nowSubscribed = toggleSubscription({ channelId: video.channelId, channelName: video.channelTitle, channelAvatar: '', }); setSubscribed(nowSubscribed); } catch (error) { console.error('Subscribe error:', error); } finally { setSubscribing(false); } }, [video?.channelId, video?.channelTitle, subscribing]); const handleSave = useCallback(() => { if (!video?.id) return; try { const nowSaved = toggleSaveVideo({ videoId: video.id, title: video.title, thumbnail: video.thumbnail, channelTitle: video.channelTitle, }); setIsSaved(nowSaved); } catch (error) { console.error('Save error:', error); } }, [video?.id, video?.title, video?.thumbnail, video?.channelTitle]); if (!video) return null; const description = video.description || ''; const hasDescription = description.length > 0; const shouldTruncate = description.length > 300; const displayDescription = expanded ? description : description.slice(0, 300) + (shouldTruncate ? '...' : ''); // Format date const formatDate = (dateStr: string) => { if (!dateStr || dateStr === 'Invalid Date') return ''; try { const date = new Date(dateStr); if (isNaN(date.getTime())) return ''; return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }); } catch { return ''; } }; // Format view count const formatViews = (views: string) => { if (!views || views === '0') return 'No views'; const num = parseInt(views.replace(/[^0-9]/g, '') || '0'); if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M views'; if (num >= 1000) return (num / 1000).toFixed(0) + 'K views'; return num.toLocaleString() + ' views'; }; return (
{/* Title */}

{video.title || 'Untitled Video'}

{/* Channel Info & Actions Row */}
{/* Channel - only show name, no avatar */}
{video.channelTitle || 'Unknown Channel'}
{/* Action Buttons - Subscribe, Share, Save */}
{/* Subscribe Button with Toggle State */} {/* Share Button */} {/* Save Button with Toggle State */}
{/* Description Box */}
{/* Views and Date */}
{formatViews(video.viewCount)} {video.publishedAt && formatDate(video.publishedAt) && ( <> {formatDate(video.publishedAt)} )}
{/* Description */} {hasDescription ? (
{displayDescription} {shouldTruncate && ( )}
) : null} {/* Tags */} {video.tags && video.tags.length > 0 && (
{video.tags.slice(0, 10).map((tag: string, i: number) => ( {tag} ))}
)}
); } // Mix Playlist Component function MixPlaylist({ videos, currentIndex, onVideoSelect, title }: { videos: VideoData[]; currentIndex: number; onVideoSelect: (index: number) => void; title?: string; }) { return (
{/* Header */}

{title || 'Mix Playlist'}

{videos.length} videos • Auto-play is on

{/* Video List */}
{videos.map((video, index) => (
onVideoSelect(index)} style={{ display: 'flex', gap: '10px', padding: '8px 12px', cursor: 'pointer', backgroundColor: index === currentIndex ? 'var(--yt-active)' : 'transparent', borderLeft: index === currentIndex ? '3px solid var(--yt-blue)' : '3px solid transparent', transition: 'background-color 0.2s', }} onMouseEnter={(e) => { if (index !== currentIndex) { (e.currentTarget as HTMLElement).style.backgroundColor = 'rgba(255,255,255,0.05)'; } }} onMouseLeave={(e) => { if (index !== currentIndex) { (e.currentTarget as HTMLElement).style.backgroundColor = 'transparent'; } }} > {/* Thumbnail with index */}
{video.title} { (e.target as HTMLImageElement).src = `https://i.ytimg.com/vi/${video.id}/default.jpg`; }} />
{index + 1}/{videos.length}
{index === currentIndex && (
)}
{/* Info */}
{video.title}
{video.uploader}
{video.duration && (
{video.duration}
)}
))}
); } // Comment Section function CommentSection({ videoId }: { videoId: string }) { const [comments, setComments] = useState([]); const [loading, setLoading] = useState(true); const [showAll, setShowAll] = useState(false); useEffect(() => { const loadComments = async () => { try { const data = await getCommentsClient(videoId, 50); setComments(data); } catch (error) { console.error('Failed to load comments:', error); } finally { setLoading(false); } }; loadComments(); }, [videoId]); if (loading) { return (
Loading comments...
); } const displayedComments = showAll ? comments : comments.slice(0, 5); return (

{comments.length} Comments

{/* Sort dropdown */}
Sort by
{/* Comments List */}
{displayedComments.map((comment) => (
{comment.author_thumbnail ? ( {comment.author} ) : null}
{comment.author} {comment.timestamp}
{comment.text}
))}
{comments.length > 5 && ( )}
); } export default function ClientWatchPage() { const searchParams = useSearchParams(); const router = useRouter(); const videoId = searchParams.get('v'); const [videoInfo, setVideoInfo] = useState(null); const [relatedVideos, setRelatedVideos] = useState([]); const [mixPlaylist, setMixPlaylist] = useState([]); const [loading, setLoading] = useState(true); const [currentIndex, setCurrentIndex] = useState(-1); const [activeTab, setActiveTab] = useState<'upnext' | 'mix'>('upnext'); const [apiError, setApiError] = useState(null); // Scroll to top when video changes or page loads useEffect(() => { window.scrollTo({ top: 0, behavior: 'instant' }); }, [videoId]); useEffect(() => { if (!videoId) return; const loadVideoData = async () => { try { setLoading(true); setApiError(null); // Check cache for video details let video = getCachedData(`video_${videoId}`); if (!video) { video = await getVideoDetailsClient(videoId); if (video) setCachedData(`video_${videoId}`, video); } setVideoInfo(video); // Add to watch history (localStorage) if (video) { addToHistory({ videoId: videoId, title: video.title, thumbnail: video.thumbnail, channelTitle: video.channelTitle, }); } // Get related videos - use channel name and video title for better results // Even if video is null, we can still try to get related videos const searchTerms = video?.title?.split(' ').filter((w: string) => w.length > 3).slice(0, 5).join(' ') || 'music'; const channelName = video?.channelTitle || ''; // Check cache for related videos const cacheKey = `related_${videoId}_${searchTerms}`; let relatedResults = getCachedData(cacheKey); let mixResults = getCachedData(`mix_${videoId}_${searchTerms}`); if (!relatedResults || !mixResults) { // Optimized: Use just 2 search requests instead of 5 to save API quota [relatedResults, mixResults] = await Promise.all([ searchVideosClient(`${channelName} ${searchTerms}`, 20), searchVideosClient(`${searchTerms} mix compilation`, 20), ]); if (relatedResults && relatedResults.length > 0) setCachedData(cacheKey, relatedResults); if (mixResults && mixResults.length > 0) setCachedData(`mix_${videoId}_${searchTerms}`, mixResults); } // Deduplicate and filter related videos - ensure arrays const uniqueRelated = Array.isArray(relatedResults) ? relatedResults.filter((v, index, self) => index === self.findIndex(item => item.id === v.id) && v.id !== videoId ) : []; setCurrentIndex(0); setRelatedVideos(uniqueRelated); // Use remaining videos for mix playlist - ensure array const uniqueMix = Array.isArray(mixResults) ? mixResults.filter((v, index, self) => index === self.findIndex(item => item.id === v.id) && v.id !== videoId && !uniqueRelated.some(r => r.id === v.id) ) : []; setMixPlaylist(uniqueMix.slice(0, 20)); // Set error message if video details failed but we have related videos if (!video) { setApiError('Video info unavailable, but you can still browse related videos.'); } } catch (error) { console.error('Failed to load video data:', error); // Fallback with fewer requests try { const fallbackResults = await searchVideosClient('music popular', 20); setRelatedVideos(Array.isArray(fallbackResults) ? fallbackResults.slice(0, 10) : []); setMixPlaylist(Array.isArray(fallbackResults) ? fallbackResults.slice(10, 20) : []); setApiError('Unable to load video details. Showing suggested videos instead.'); } catch (e: any) { console.error('Fallback also failed:', e); // Set empty arrays to show user-friendly message setRelatedVideos([]); setMixPlaylist([]); // Set user-friendly error message if (e?.message?.includes('quota exceeded')) { setApiError('YouTube API quota exceeded. Please try again later.'); } else if (e?.message?.includes('API key')) { setApiError('API key issue. Please check configuration.'); } else { setApiError('Unable to load related videos. Please try again.'); } } } finally { setLoading(false); } }; loadVideoData(); }, [videoId]); const handleVideoSelect = (index: number) => { const video = activeTab === 'upnext' ? relatedVideos[index] : mixPlaylist[index]; if (video) { router.push(`/watch?v=${video.id}`); } }; const handlePrevious = () => { if (currentIndex > 0) { const prevVideo = relatedVideos[currentIndex - 1]; router.push(`/watch?v=${prevVideo.id}`); } }; const handleNext = () => { const playlist = activeTab === 'mix' ? mixPlaylist : relatedVideos; if (currentIndex < playlist.length - 1) { const nextVideo = playlist[currentIndex + 1]; router.push(`/watch?v=${nextVideo.id}`); } }; const handleVideoEnd = () => { const playlist = activeTab === 'mix' ? mixPlaylist : relatedVideos; if (currentIndex < playlist.length - 1) { handleNext(); } }; if (!videoId) { return
No video ID provided
; } if (loading) { return ; } const currentPlaylist = activeTab === 'mix' ? mixPlaylist : relatedVideos; return (
{/* Main Content */}
{/* Video Player */}
{/* Player Controls */}
{/* Video Info */} {/* Comments */}
{/* Sidebar */}
{/* Mix Playlist - Always on top */} {/* API Error Message */} {apiError && (
{apiError}
)} {/* Up Next Section */}

Up Next

{relatedVideos.length} videos
{relatedVideos.slice(0, 8).map((video, index) => (
handleVideoSelect(index)} style={{ display: 'flex', gap: '10px', padding: '8px 12px', cursor: 'pointer', backgroundColor: index === currentIndex ? 'var(--yt-active)' : 'transparent', borderLeft: index === currentIndex ? '3px solid var(--yt-blue)' : '3px solid transparent', transition: 'background-color 0.2s', }} onMouseEnter={(e) => { if (index !== currentIndex) { (e.currentTarget as HTMLElement).style.backgroundColor = 'rgba(255,255,255,0.05)'; } }} onMouseLeave={(e) => { if (index !== currentIndex) { (e.currentTarget as HTMLElement).style.backgroundColor = 'transparent'; } }} >
{video.title} { (e.target as HTMLImageElement).src = `https://i.ytimg.com/vi/${video.id}/mqdefault.jpg`; }} /> {video.duration && (
{video.duration}
)}
{video.title}
{video.uploader}
))}
{/* Responsive styles */}
); }