diff --git a/app/favorites/page.tsx b/app/favorites/page.tsx new file mode 100644 index 0000000..2402e97 --- /dev/null +++ b/app/favorites/page.tsx @@ -0,0 +1,15 @@ +'use client'; + +import { Suspense } from 'react'; +import { + FavoritesPageContent, + FavoritesPageFallback, +} from '@/components/favorites/FavoritesPageContent'; + +export default function Favorites() { + return ( + }> + + + ); +} diff --git a/app/premium/favorites/page.tsx b/app/premium/favorites/page.tsx new file mode 100644 index 0000000..758acbf --- /dev/null +++ b/app/premium/favorites/page.tsx @@ -0,0 +1,15 @@ +'use client'; + +import { Suspense } from 'react'; +import { + FavoritesPageContent, + FavoritesPageFallback, +} from '@/components/favorites/FavoritesPageContent'; + +export default function PremiumFavorites() { + return ( + }> + + + ); +} diff --git a/components/favorites/FavoritesGrid.tsx b/components/favorites/FavoritesGrid.tsx new file mode 100644 index 0000000..6ca3ec9 --- /dev/null +++ b/components/favorites/FavoritesGrid.tsx @@ -0,0 +1,109 @@ +'use client'; + +import { useState, useRef, useCallback, useEffect, useMemo, memo } from 'react'; +import { VideoCard } from '@/components/search/VideoCard'; +import { FavoritesEmptyState } from './FavoritesEmptyState'; +import type { FavoriteItem, Video } from '@/lib/types'; + +interface FavoritesGridProps { + favorites: FavoriteItem[]; + isPremium?: boolean; +} + +export const FavoritesGrid = memo(function FavoritesGrid({ + favorites, + isPremium = false +}: FavoritesGridProps) { + const [activeCardId, setActiveCardId] = useState(null); + const [visibleCount, setVisibleCount] = useState(24); + const loadMoreRef = useRef(null); + const observerRef = useRef(null); + + // Convert FavoriteItem to Video format + const videos: Video[] = useMemo( + () => + favorites.map((favorite) => ({ + vod_id: favorite.videoId, + vod_name: favorite.title, + vod_pic: favorite.poster, + vod_remarks: favorite.remarks, + vod_year: favorite.year, + type_name: favorite.type, + source: favorite.source, + sourceName: favorite.sourceName, + })), + [favorites] + ); + + // Setup intersection observer for infinite scroll + useEffect(() => { + if (observerRef.current) { + observerRef.current.disconnect(); + } + + observerRef.current = new IntersectionObserver( + (entries) => { + const first = entries[0]; + if (first.isIntersecting && visibleCount < videos.length) { + setVisibleCount((prev) => Math.min(prev + 24, videos.length)); + } + }, + { threshold: 0.1 } + ); + + if (loadMoreRef.current) { + observerRef.current.observe(loadMoreRef.current); + } + + return () => { + if (observerRef.current) { + observerRef.current.disconnect(); + } + }; + }, [visibleCount, videos.length]); + + const handleCardClick = useCallback(( + e: React.MouseEvent, + cardId: string, + videoUrl: string + ) => { + setActiveCardId(cardId); + }, []); + + if (videos.length === 0) { + return ; + } + + return ( + <> +
+ {videos.slice(0, visibleCount).map((video) => { + const cardId = `${video.source}:${video.vod_id}`; + const videoUrl = `/player?id=${video.vod_id}&source=${video.source}&title=${encodeURIComponent(video.vod_name)}${isPremium ? '&premium=1' : ''}`; + const isActive = activeCardId === cardId; + + return ( + + ); + })} +
+ + {/* Load more trigger */} + {visibleCount < videos.length && ( +