From 08cc9397e5a6f4c36b3c864e1823f9344a53f25f Mon Sep 17 00:00:00 2001 From: tapframe Date: Mon, 7 Jul 2025 16:59:29 +0530 Subject: [PATCH] implemented cast details modal --- src/components/metadata/CastDetailsModal.tsx | 548 +++++++++++++++++++ src/components/metadata/HeroSection.tsx | 7 - src/screens/MetadataScreen.tsx | 15 +- 3 files changed, 562 insertions(+), 8 deletions(-) create mode 100644 src/components/metadata/CastDetailsModal.tsx diff --git a/src/components/metadata/CastDetailsModal.tsx b/src/components/metadata/CastDetailsModal.tsx new file mode 100644 index 00000000..074ff15b --- /dev/null +++ b/src/components/metadata/CastDetailsModal.tsx @@ -0,0 +1,548 @@ +import React, { useState, useEffect } from 'react'; +import { + View, + Text, + TouchableOpacity, + ScrollView, + ActivityIndicator, + Dimensions, + Platform, +} from 'react-native'; +import { MaterialIcons } from '@expo/vector-icons'; +import { BlurView } from 'expo-blur'; +import { Image } from 'expo-image'; +import Animated, { + FadeIn, + FadeOut, + useAnimatedStyle, + useSharedValue, + withTiming, + runOnJS, +} from 'react-native-reanimated'; +import { LinearGradient } from 'expo-linear-gradient'; +import { useTheme } from '../../contexts/ThemeContext'; +import { Cast } from '../../types/cast'; +import { tmdbService } from '../../services/tmdbService'; + +interface CastDetailsModalProps { + visible: boolean; + onClose: () => void; + castMember: Cast | null; +} + +const { width, height } = Dimensions.get('window'); +const MODAL_WIDTH = Math.min(width - 32, 520); +const MODAL_MAX_HEIGHT = height * 0.85; + +interface PersonDetails { + id: number; + name: string; + biography: string; + birthday: string | null; + place_of_birth: string | null; + known_for_department: string; + profile_path: string | null; + also_known_as: string[]; +} + +const InfoBadge = ({ + label, + value, + icon, + color = '#6B7280', + bgColor = 'rgba(107, 114, 128, 0.15)' +}: { + label: string; + value: string; + icon?: string; + color?: string; + bgColor?: string; +}) => ( + + {icon && ( + + )} + + + {label} + + + {value} + + + +); + +export const CastDetailsModal: React.FC = ({ + visible, + onClose, + castMember, +}) => { + const { currentTheme } = useTheme(); + const [personDetails, setPersonDetails] = useState(null); + const [loading, setLoading] = useState(false); + const modalOpacity = useSharedValue(0); + + useEffect(() => { + if (visible && castMember) { + modalOpacity.value = withTiming(1, { duration: 200 }); + fetchPersonDetails(); + } else { + modalOpacity.value = withTiming(0, { duration: 150 }); + } + }, [visible, castMember]); + + const fetchPersonDetails = async () => { + if (!castMember) return; + + setLoading(true); + try { + const details = await tmdbService.getPersonDetails(castMember.id); + setPersonDetails(details); + } catch (error) { + console.error('Error fetching person details:', error); + } finally { + setLoading(false); + } + }; + + const modalStyle = useAnimatedStyle(() => ({ + opacity: modalOpacity.value, + })); + + const handleClose = () => { + modalOpacity.value = withTiming(0, { duration: 150 }, () => { + runOnJS(onClose)(); + }); + }; + + const formatDate = (dateString: string | null) => { + if (!dateString) return null; + const date = new Date(dateString); + return date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + }); + }; + + const calculateAge = (birthday: string | null) => { + if (!birthday) return null; + const today = new Date(); + const birthDate = new Date(birthday); + let age = today.getFullYear() - birthDate.getFullYear(); + const monthDiff = today.getMonth() - birthDate.getMonth(); + + if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { + age--; + } + + return age; + }; + + if (!visible || !castMember) return null; + + return ( + + + + + + + + + {castMember.profile_path ? ( + + ) : ( + + + {castMember.name.split(' ').reduce((prev: string, current: string) => prev + current[0], '').substring(0, 2)} + + + )} + + + + + {castMember.name} + + {castMember.character && ( + + as {castMember.character} + + )} + + + + + + + + + + {loading ? ( + + + + Loading details... + + + ) : ( + + {/* Personal Information */} + + + Personal Information + + + + {personDetails?.known_for_department && ( + + )} + + {personDetails?.birthday && ( + + )} + + {personDetails?.place_of_birth && ( + + )} + + + {personDetails?.birthday && ( + + + + + Born + + + + {formatDate(personDetails.birthday)} + + + )} + + + {/* Biography */} + {personDetails?.biography && ( + + + Biography + + + + + {personDetails.biography} + + + + )} + + {/* Also Known As */} + {personDetails?.also_known_as && personDetails.also_known_as.length > 0 && ( + + + Also Known As + + + + {personDetails.also_known_as.slice(0, 6).map((alias, index) => ( + + + {alias} + + + ))} + + + )} + + {/* No details available */} + {!loading && !personDetails?.biography && !personDetails?.birthday && !personDetails?.place_of_birth && ( + + + + No additional details available + + + )} + + )} + + + + + ); +}; + +export default CastDetailsModal; \ No newline at end of file diff --git a/src/components/metadata/HeroSection.tsx b/src/components/metadata/HeroSection.tsx index a2e00cb0..e1093708 100644 --- a/src/components/metadata/HeroSection.tsx +++ b/src/components/metadata/HeroSection.tsx @@ -210,13 +210,6 @@ const ActionButtons = React.memo(({ color={isWatched ? "#fff" : "#000"} /> {finalPlayButtonText} - - {/* Subtle watched indicator in play button */} - {isWatched && ( - - - - )} { // Optimized state management - reduced state variables const [isContentReady, setIsContentReady] = useState(false); + const [showCastModal, setShowCastModal] = useState(false); + const [selectedCastMember, setSelectedCastMember] = useState(null); const transitionOpacity = useSharedValue(1); const { @@ -310,7 +313,10 @@ const MetadataScreen: React.FC = () => { }, [navigation, id, type]); const handleBack = useCallback(() => navigation.goBack(), [navigation]); - const handleSelectCastMember = useCallback(() => {}, []); // Simplified for performance + const handleSelectCastMember = useCallback((castMember: any) => { + setSelectedCastMember(castMember); + setShowCastModal(true); + }, []); // Ultra-optimized animated styles - minimal calculations const containerStyle = useAnimatedStyle(() => ({ @@ -472,6 +478,13 @@ const MetadataScreen: React.FC = () => { )} + + {/* Cast Details Modal */} + setShowCastModal(false)} + castMember={selectedCastMember} + /> ); };