From 7aa66aff7474a5ef14923e204c785130ba7880d2 Mon Sep 17 00:00:00 2001 From: tapframe Date: Wed, 5 Nov 2025 14:10:58 +0530 Subject: [PATCH] improved hero --- src/components/home/HeroCarousel.tsx | 285 ++++++++++++++++++++------- 1 file changed, 217 insertions(+), 68 deletions(-) diff --git a/src/components/home/HeroCarousel.tsx b/src/components/home/HeroCarousel.tsx index 6a74f171b..ba09cf2f8 100644 --- a/src/components/home/HeroCarousel.tsx +++ b/src/components/home/HeroCarousel.tsx @@ -5,6 +5,7 @@ import { LinearGradient } from 'expo-linear-gradient'; import { BlurView } from 'expo-blur'; import FastImage from '@d11/react-native-fast-image'; import { Pagination } from 'react-native-reanimated-carousel'; +import { Ionicons } from '@expo/vector-icons'; // Optional iOS Glass effect (expo-glass-effect) with safe fallback for HeroCarousel let GlassViewComp: any = null; @@ -56,6 +57,10 @@ const HeroCarousel: React.FC = ({ items, loading = false }) = const [activeIndex, setActiveIndex] = useState(0); const [failedLogoIds, setFailedLogoIds] = useState>(new Set()); const scrollViewRef = useRef(null); + const [flippedMap, setFlippedMap] = useState>({}); + const toggleFlipById = useCallback((id: string) => { + setFlippedMap((prev) => ({ ...prev, [id]: !prev[id] })); + }, []); // Note: do not early-return before hooks. Loading UI is returned later. @@ -368,6 +373,8 @@ const HeroCarousel: React.FC = ({ items, loading = false }) = onPressInfo={() => handleNavigateToMetadata(item.id, item.type)} scrollX={scrollX} index={index} + flipped={!!flippedMap[item.id]} + onToggleFlip={() => toggleFlipById(item.id)} /> ))} @@ -419,9 +426,11 @@ interface CarouselCardProps { onPressInfo: () => void; scrollX: SharedValue; index: number; + flipped: boolean; + onToggleFlip: () => void; } -const CarouselCard: React.FC = memo(({ item, colors, logoFailed, onLogoError, onPressInfo, scrollX, index }) => { +const CarouselCard: React.FC = memo(({ item, colors, logoFailed, onLogoError, onPressInfo, scrollX, index, flipped, onToggleFlip }) => { const [bannerLoaded, setBannerLoaded] = useState(false); const [logoLoaded, setLogoLoaded] = useState(false); @@ -429,6 +438,7 @@ const CarouselCard: React.FC = memo(({ item, colors, logoFail const logoOpacity = useSharedValue(0); const genresOpacity = useSharedValue(0); const actionsOpacity = useSharedValue(0); + const isFlipped = useSharedValue(flipped ? 1 : 0); // Reset animations when component mounts/remounts to prevent glitching useEffect(() => { @@ -455,6 +465,32 @@ const CarouselCard: React.FC = memo(({ item, colors, logoFail opacity: logoOpacity.value, })); + // Flip styles + const frontFlipStyle = useAnimatedStyle(() => { + const rotate = interpolate(isFlipped.value, [0, 1], [0, 180]); + return { + transform: [ + { perspective: 1000 }, + { rotateY: `${rotate}deg` }, + ], + } as any; + }); + + const backFlipStyle = useAnimatedStyle(() => { + const rotate = interpolate(isFlipped.value, [0, 1], [-180, 0]); + return { + transform: [ + { perspective: 1000 }, + { rotateY: `${rotate}deg` }, + ], + } as any; + }); + + // Sync animation with prop changes + useEffect(() => { + isFlipped.value = withTiming(flipped ? 1 : 0, { duration: 300, easing: Easing.out(Easing.cubic) }); + }, [flipped]); + // ULTRA-OPTIMIZED: Only animate the center card and ±1 neighbors // Use a simple distance-based approach instead of reading scrollX.value during render const shouldAnimate = useMemo(() => { @@ -568,14 +604,8 @@ const CarouselCard: React.FC = memo(({ item, colors, logoFail }, [logoLoaded]); return ( - - + + = memo(({ item, colors, logoFail borderColor: 'rgba(255,255,255,0.18)', } ] as StyleProp}> - - {!bannerLoaded && ( - - )} - + {/* FRONT FACE */} + + + + {!bannerLoaded && ( + + )} + + setBannerLoaded(true)} + /> + + + + {item.logo && !logoFailed ? ( + + + setLogoLoaded(true)} + onError={onLogoError} + /> + + + ) : ( + + + + {item.name} + + + + )} + {item.genres && ( + + + + {item.genres.slice(0, 3).join(' • ')} + + + + )} + + + + {/* BACK FACE */} + + setBannerLoaded(true)} /> - - + + + + {item.logo && !logoFailed ? ( + + ) : ( + + {item.name} + + )} + {item.year && ( + + + + {item.year} + + + )} + + + {item.description || 'No description available'} + + + + + + {/* FLIP BUTTON */} + + + + - {/* Static genres positioned absolutely over the card */} - {item.genres && ( - - - - {item.genres.slice(0, 3).join(' • ')} - - - - )} - {/* Static logo positioned absolutely over the card */} - {item.logo && !logoFailed && ( - - - setLogoLoaded(true)} - onError={onLogoError} - /> - - - )} - {/* Static title when no logo */} - {!item.logo || logoFailed ? ( - - - - {item.name} - - - - ) : null} - + ); }); @@ -697,6 +781,21 @@ const styles = StyleSheet.create({ shadowOpacity: 0.15, shadowRadius: 4, }, + flipFace: { + position: 'absolute', + left: 0, + right: 0, + top: 0, + bottom: 0, + backfaceVisibility: 'hidden', + }, + frontFace: { + // front specific adjustments if needed + }, + backFace: { + // back specific adjustments if needed + backfaceVisibility: 'hidden', + }, skeletonCard: { width: CARD_WIDTH, height: CARD_HEIGHT, @@ -756,6 +855,22 @@ const styles = StyleSheet.create({ bottom: 0, top: 0, }, + flipButtonContainer: { + position: 'absolute', + top: 10, + right: 10, + zIndex: 10, + }, + flipButton: { + width: 32, + height: 32, + borderRadius: 16, + backgroundColor: 'rgba(0,0,0,0.45)', + alignItems: 'center', + justifyContent: 'center', + borderWidth: 1, + borderColor: 'rgba(255,255,255,0.18)' + }, info: { position: 'absolute', @@ -797,6 +912,40 @@ const styles = StyleSheet.create({ justifyContent: 'flex-end', paddingBottom: 40, // Position above genres }, + backContent: { + flex: 1, + alignItems: 'center', + justifyContent: 'flex-end', + paddingHorizontal: 16, + paddingBottom: 16, + gap: 8, + }, + backTitle: { + fontSize: 18, + fontWeight: '800', + marginBottom: 6, + textAlign: 'center', + }, + backDescription: { + fontSize: 14, + lineHeight: 20, + textAlign: 'center', + }, + infoRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gap: 12, + marginBottom: 6, + }, + infoItem: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + }, + infoText: { + fontSize: 13, + }, titleOverlay: { position: 'absolute', left: 0,