diff --git a/src/components/home/HeroCarousel.tsx b/src/components/home/HeroCarousel.tsx index 41ab431..bf29062 100644 --- a/src/components/home/HeroCarousel.tsx +++ b/src/components/home/HeroCarousel.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState, useEffect } from 'react'; +import React, { useMemo, useState, useEffect, useCallback, memo } from 'react'; import { View, Text, StyleSheet, Dimensions, TouchableOpacity, ViewStyle, TextStyle, ImageStyle, FlatList, StyleProp } from 'react-native'; import Animated, { FadeIn, FadeOut, Easing, useSharedValue, withTiming, useAnimatedStyle } from 'react-native-reanimated'; import { LinearGradient } from 'expo-linear-gradient'; @@ -30,6 +30,44 @@ const HeroCarousel: React.FC = ({ items, loading = false }) = const [activeIndex, setActiveIndex] = useState(0); const [failedLogoIds, setFailedLogoIds] = useState>(new Set()); + // Note: do not early-return before hooks. Loading UI is returned later. + + const hasData = data.length > 0; + + const handleMomentumEnd = useCallback((event: any) => { + const offsetX = event?.nativeEvent?.contentOffset?.x ?? 0; + const interval = CARD_WIDTH + 16; + const idx = Math.round(offsetX / interval); + const clamped = Math.max(0, Math.min(idx, data.length - 1)); + if (clamped !== activeIndex) { + // Small delay to ensure smooth transition + setTimeout(() => { + setActiveIndex(clamped); + }, 50); + } + }, [activeIndex, data.length]); + + const contentPadding = useMemo(() => ({ paddingHorizontal: (width - CARD_WIDTH) / 2 }), []); + + const keyExtractor = useCallback((item: StreamingContent) => item.id, []); + + const getItemLayout = useCallback( + (_: unknown, index: number) => { + const length = CARD_WIDTH + 16; + const offset = length * index; + return { length, offset, index }; + }, + [] + ); + + const handleNavigateToMetadata = useCallback((id: string, type: any) => { + navigation.navigate('Metadata', { id, type }); + }, [navigation]); + + const handleNavigateToStreams = useCallback((id: string, type: any) => { + navigation.navigate('Streams', { id, type }); + }, [navigation]); + if (loading) { return ( }> @@ -70,31 +108,6 @@ const HeroCarousel: React.FC = ({ items, loading = false }) = ); } - if (data.length === 0) return null; - - const handleMomentumEnd = (event: any) => { - const offsetX = event?.nativeEvent?.contentOffset?.x ?? 0; - const interval = CARD_WIDTH + 16; - const idx = Math.round(offsetX / interval); - const clamped = Math.max(0, Math.min(idx, data.length - 1)); - if (clamped !== activeIndex) { - // Small delay to ensure smooth transition - setTimeout(() => { - setActiveIndex(clamped); - }, 50); - } - }; - - const handleScroll = (event: any) => { - const offsetX = event?.nativeEvent?.contentOffset?.x ?? 0; - const interval = CARD_WIDTH + 16; - const idx = Math.round(offsetX / interval); - const clamped = Math.max(0, Math.min(idx, data.length - 1)); - if (clamped !== activeIndex) { - setActiveIndex(clamped); - } - }; - // Memoized background component with improved timing const BackgroundImage = React.memo(({ item, @@ -146,6 +159,8 @@ const HeroCarousel: React.FC = ({ items, loading = false }) = ); }); + if (!hasData) return null; + return ( @@ -186,78 +201,29 @@ const HeroCarousel: React.FC = ({ items, loading = false }) = /> item.id} + keyExtractor={keyExtractor} horizontal showsHorizontalScrollIndicator={false} snapToInterval={CARD_WIDTH + 16} decelerationRate="fast" - contentContainerStyle={{ paddingHorizontal: (width - CARD_WIDTH) / 2 }} - onScroll={handleScroll} + contentContainerStyle={contentPadding} onMomentumScrollEnd={handleMomentumEnd} + initialNumToRender={3} + windowSize={3} + maxToRenderPerBatch={3} + updateCellsBatchingPeriod={50} + removeClippedSubviews + getItemLayout={getItemLayout} renderItem={({ item }) => ( - navigation.navigate('Metadata', { id: item.id, type: item.type })} - > - }> - - - - - - {item.logo && !failedLogoIds.has(item.id) ? ( - { - setFailedLogoIds((prev) => new Set(prev).add(item.id)); - }} - /> - ) : ( - - {item.name} - - )} - {item.genres && ( - - {item.genres.slice(0, 3).join(' • ')} - - )} - - navigation.navigate('Streams', { id: item.id, type: item.type })} - activeOpacity={0.85} - > - - Play - - navigation.navigate('Metadata', { id: item.id, type: item.type })} - activeOpacity={0.8} - > - - Info - - - - - + setFailedLogoIds((prev) => new Set(prev).add(item.id))} + onPressInfo={() => handleNavigateToMetadata(item.id, item.type)} + onPressPlay={() => handleNavigateToStreams(item.id, item.type)} + /> )} /> @@ -266,6 +232,80 @@ const HeroCarousel: React.FC = ({ items, loading = false }) = ); }; +interface CarouselCardProps { + item: StreamingContent; + colors: any; + logoFailed: boolean; + onLogoError: () => void; + onPressPlay: () => void; + onPressInfo: () => void; +} + +const CarouselCard: React.FC = memo(({ item, colors, logoFailed, onLogoError, onPressPlay, onPressInfo }) => { + return ( + + }> + + + + + + {item.logo && !logoFailed ? ( + + ) : ( + + {item.name} + + )} + {item.genres && ( + + {item.genres.slice(0, 3).join(' • ')} + + )} + + + + Play + + + + Info + + + + + + ); +}); + const styles = StyleSheet.create({ container: { paddingVertical: 12,