cast modal update

This commit is contained in:
tapframe 2025-07-07 17:13:24 +05:30
parent 08cc9397e5
commit 90a09ac5a2

View file

@ -17,6 +17,7 @@ import Animated, {
useAnimatedStyle, useAnimatedStyle,
useSharedValue, useSharedValue,
withTiming, withTiming,
withSpring,
runOnJS, runOnJS,
} from 'react-native-reanimated'; } from 'react-native-reanimated';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
@ -31,8 +32,8 @@ interface CastDetailsModalProps {
} }
const { width, height } = Dimensions.get('window'); const { width, height } = Dimensions.get('window');
const MODAL_WIDTH = Math.min(width - 32, 520); const MODAL_WIDTH = Math.min(width - 40, 400);
const MODAL_MAX_HEIGHT = height * 0.85; const MODAL_HEIGHT = height * 0.7;
interface PersonDetails { interface PersonDetails {
id: number; id: number;
@ -45,57 +46,6 @@ interface PersonDetails {
also_known_as: string[]; 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;
}) => (
<View style={{
backgroundColor: bgColor,
borderColor: `${color}40`,
borderWidth: 1,
paddingHorizontal: 12,
paddingVertical: 8,
borderRadius: 12,
flexDirection: 'row',
alignItems: 'center',
marginBottom: 8,
marginRight: 8,
}}>
{icon && (
<MaterialIcons name={icon as any} size={14} color={color} style={{ marginRight: 6 }} />
)}
<View>
<Text style={{
color: color,
fontSize: 10,
fontWeight: '600',
letterSpacing: 0.3,
textTransform: 'uppercase',
marginBottom: 2,
}}>
{label}
</Text>
<Text style={{
color: '#fff',
fontSize: 12,
fontWeight: '700',
letterSpacing: -0.1,
}}>
{value}
</Text>
</View>
</View>
);
export const CastDetailsModal: React.FC<CastDetailsModalProps> = ({ export const CastDetailsModal: React.FC<CastDetailsModalProps> = ({
visible, visible,
onClose, onClose,
@ -104,24 +54,37 @@ export const CastDetailsModal: React.FC<CastDetailsModalProps> = ({
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
const [personDetails, setPersonDetails] = useState<PersonDetails | null>(null); const [personDetails, setPersonDetails] = useState<PersonDetails | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [hasFetched, setHasFetched] = useState(false);
const modalOpacity = useSharedValue(0); const modalOpacity = useSharedValue(0);
const modalScale = useSharedValue(0.9);
useEffect(() => { useEffect(() => {
if (visible && castMember) { if (visible && castMember) {
modalOpacity.value = withTiming(1, { duration: 200 }); modalOpacity.value = withTiming(1, { duration: 250 });
fetchPersonDetails(); modalScale.value = withSpring(1, { damping: 20, stiffness: 200 });
if (!hasFetched || personDetails?.id !== castMember.id) {
fetchPersonDetails();
}
} else { } else {
modalOpacity.value = withTiming(0, { duration: 150 }); modalOpacity.value = withTiming(0, { duration: 200 });
modalScale.value = withTiming(0.9, { duration: 200 });
if (!visible) {
setHasFetched(false);
setPersonDetails(null);
}
} }
}, [visible, castMember]); }, [visible, castMember]);
const fetchPersonDetails = async () => { const fetchPersonDetails = async () => {
if (!castMember) return; if (!castMember || loading) return;
setLoading(true); setLoading(true);
try { try {
const details = await tmdbService.getPersonDetails(castMember.id); const details = await tmdbService.getPersonDetails(castMember.id);
setPersonDetails(details); setPersonDetails(details);
setHasFetched(true);
} catch (error) { } catch (error) {
console.error('Error fetching person details:', error); console.error('Error fetching person details:', error);
} finally { } finally {
@ -131,10 +94,12 @@ export const CastDetailsModal: React.FC<CastDetailsModalProps> = ({
const modalStyle = useAnimatedStyle(() => ({ const modalStyle = useAnimatedStyle(() => ({
opacity: modalOpacity.value, opacity: modalOpacity.value,
transform: [{ scale: modalScale.value }],
})); }));
const handleClose = () => { const handleClose = () => {
modalOpacity.value = withTiming(0, { duration: 150 }, () => { modalOpacity.value = withTiming(0, { duration: 200 });
modalScale.value = withTiming(0.9, { duration: 200 }, () => {
runOnJS(onClose)(); runOnJS(onClose)();
}); });
}; };
@ -144,7 +109,7 @@ export const CastDetailsModal: React.FC<CastDetailsModalProps> = ({
const date = new Date(dateString); const date = new Date(dateString);
return date.toLocaleDateString('en-US', { return date.toLocaleDateString('en-US', {
year: 'numeric', year: 'numeric',
month: 'long', month: 'short',
day: 'numeric', day: 'numeric',
}); });
}; };
@ -167,19 +132,19 @@ export const CastDetailsModal: React.FC<CastDetailsModalProps> = ({
return ( return (
<Animated.View <Animated.View
entering={FadeIn.duration(200)} entering={FadeIn.duration(250)}
exiting={FadeOut.duration(150)} exiting={FadeOut.duration(200)}
style={{ style={{
position: 'absolute', position: 'absolute',
top: 0, top: 0,
left: 0, left: 0,
right: 0, right: 0,
bottom: 0, bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.9)', backgroundColor: 'rgba(0, 0, 0, 0.85)',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
zIndex: 9999, zIndex: 9999,
padding: 16, padding: 20,
}} }}
> >
<TouchableOpacity <TouchableOpacity
@ -198,351 +163,310 @@ export const CastDetailsModal: React.FC<CastDetailsModalProps> = ({
style={[ style={[
{ {
width: MODAL_WIDTH, width: MODAL_WIDTH,
maxHeight: MODAL_MAX_HEIGHT, height: MODAL_HEIGHT,
minHeight: height * 0.4,
overflow: 'hidden', overflow: 'hidden',
elevation: 25, borderRadius: 24,
shadowColor: '#000', backgroundColor: Platform.OS === 'android'
shadowOffset: { width: 0, height: 12 }, ? 'rgba(20, 20, 20, 0.95)'
shadowOpacity: 0.4, : 'transparent',
shadowRadius: 25,
alignSelf: 'center',
}, },
modalStyle, modalStyle,
]} ]}
> >
<BlurView {Platform.OS === 'ios' ? (
intensity={100} <BlurView
tint="dark" intensity={100}
style={{ tint="dark"
borderRadius: 28,
overflow: 'hidden',
backgroundColor: 'rgba(26, 26, 26, 0.8)',
width: '100%',
height: '100%',
}}
>
<LinearGradient
colors={[
currentTheme.colors.primary + '95',
currentTheme.colors.primaryVariant + '95',
currentTheme.colors.primaryVariant + '90',
]}
locations={[0, 0.6, 1]}
style={{ style={{
paddingHorizontal: 28,
paddingVertical: 24,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
borderBottomWidth: 1,
borderBottomColor: 'rgba(255, 255, 255, 0.1)',
width: '100%', width: '100%',
height: '100%',
backgroundColor: 'rgba(20, 20, 20, 0.8)',
}} }}
> >
<View style={{ flex: 1, flexDirection: 'row', alignItems: 'center' }}> {renderContent()}
<View style={{ </BlurView>
width: 60, ) : (
height: 60, renderContent()
borderRadius: 30, )}
overflow: 'hidden', </Animated.View>
marginRight: 16, </Animated.View>
backgroundColor: 'rgba(255, 255, 255, 0.1)', );
}}>
{castMember.profile_path ? ( function renderContent() {
<Image return (
source={{ <>
uri: `https://image.tmdb.org/t/p/w185${castMember.profile_path}`, {/* Header */}
}} <LinearGradient
style={{ width: '100%', height: '100%' }} colors={[
contentFit="cover" currentTheme.colors.primary + 'DD',
/> currentTheme.colors.primaryVariant + 'CC',
) : ( ]}
<View style={{ style={{
width: '100%', padding: 20,
height: '100%', paddingTop: 24,
alignItems: 'center', }}
justifyContent: 'center', >
backgroundColor: 'rgba(255, 255, 255, 0.15)', <View style={{ flexDirection: 'row', alignItems: 'center' }}>
}}> <View style={{
<Text style={{ width: 60,
color: '#fff', height: 60,
fontSize: 18, borderRadius: 30,
fontWeight: '700', overflow: 'hidden',
}}> marginRight: 16,
{castMember.name.split(' ').reduce((prev: string, current: string) => prev + current[0], '').substring(0, 2)} backgroundColor: 'rgba(255, 255, 255, 0.1)',
</Text> }}>
</View> {castMember.profile_path ? (
)} <Image
</View> source={{
uri: `https://image.tmdb.org/t/p/w185${castMember.profile_path}`,
<View style={{ flex: 1 }}> }}
<Text style={{ style={{ width: '100%', height: '100%' }}
color: '#fff', contentFit="cover"
fontSize: 20, />
fontWeight: '800', ) : (
letterSpacing: -0.6, <View style={{
textShadowColor: 'rgba(0, 0, 0, 0.3)', width: '100%',
textShadowOffset: { width: 0, height: 1 }, height: '100%',
textShadowRadius: 2, alignItems: 'center',
}} numberOfLines={1}> justifyContent: 'center',
{castMember.name} }}>
</Text>
{castMember.character && (
<Text style={{ <Text style={{
color: 'rgba(255, 255, 255, 0.85)', color: '#fff',
fontSize: 14, fontSize: 18,
marginTop: 4, fontWeight: '700',
fontWeight: '500', }}>
letterSpacing: 0.2, {castMember.name.split(' ').reduce((prev: string, current: string) => prev + current[0], '').substring(0, 2)}
}} numberOfLines={1}>
as {castMember.character}
</Text> </Text>
)} </View>
</View> )}
</View>
<View style={{ flex: 1 }}>
<Text style={{
color: '#fff',
fontSize: 18,
fontWeight: '800',
marginBottom: 4,
}} numberOfLines={2}>
{castMember.name}
</Text>
{castMember.character && (
<Text style={{
color: 'rgba(255, 255, 255, 0.8)',
fontSize: 14,
fontWeight: '500',
}} numberOfLines={2}>
as {castMember.character}
</Text>
)}
</View> </View>
<TouchableOpacity <TouchableOpacity
style={{ style={{
width: 44, width: 36,
height: 44, height: 36,
borderRadius: 22, borderRadius: 18,
backgroundColor: 'rgba(255, 255, 255, 0.15)', backgroundColor: 'rgba(255, 255, 255, 0.2)',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
marginLeft: 16,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.2)',
}} }}
onPress={handleClose} onPress={handleClose}
activeOpacity={0.7} activeOpacity={0.7}
> >
<MaterialIcons name="close" size={20} color="#fff" /> <MaterialIcons name="close" size={20} color="#fff" />
</TouchableOpacity> </TouchableOpacity>
</LinearGradient> </View>
</LinearGradient>
<ScrollView {/* Content */}
style={{ <ScrollView
maxHeight: MODAL_MAX_HEIGHT - 120, style={{ flex: 1 }}
backgroundColor: 'transparent', contentContainerStyle={{ padding: 20 }}
width: '100%', showsVerticalScrollIndicator={false}
}} >
showsVerticalScrollIndicator={false} {loading ? (
contentContainerStyle={{ <View style={{
padding: 24, alignItems: 'center',
paddingBottom: 32, justifyContent: 'center',
width: '100%', paddingVertical: 40,
}} }}>
bounces={false} <ActivityIndicator size="large" color={currentTheme.colors.primary} />
> <Text style={{
{loading ? ( color: 'rgba(255, 255, 255, 0.7)',
<View style={{ fontSize: 14,
alignItems: 'center', marginTop: 12,
justifyContent: 'center',
paddingVertical: 40,
}}> }}>
<ActivityIndicator size="large" color={currentTheme.colors.primary} /> Loading details...
<Text style={{ </Text>
color: 'rgba(255, 255, 255, 0.7)', </View>
fontSize: 16, ) : (
marginTop: 16, <View>
fontWeight: '500', {/* Quick Info */}
{(personDetails?.known_for_department || personDetails?.birthday || personDetails?.place_of_birth) && (
<View style={{
backgroundColor: 'rgba(255, 255, 255, 0.05)',
borderRadius: 16,
padding: 16,
marginBottom: 20,
}}> }}>
Loading details... {personDetails?.known_for_department && (
</Text> <View style={{
</View> flexDirection: 'row',
) : ( alignItems: 'center',
<View style={{ width: '100%' }}> marginBottom: personDetails?.birthday || personDetails?.place_of_birth ? 12 : 0
{/* Personal Information */} }}>
<View style={{ marginBottom: 24 }}> <MaterialIcons name="work" size={16} color={currentTheme.colors.primary} />
<Text style={{ <Text style={{
color: 'rgba(255, 255, 255, 0.7)', color: 'rgba(255, 255, 255, 0.7)',
fontSize: 14, fontSize: 12,
fontWeight: '600', marginLeft: 8,
letterSpacing: 0.3, marginRight: 12,
marginBottom: 16, }}>
textTransform: 'uppercase', Department
}}> </Text>
Personal Information <Text style={{
</Text> color: '#fff',
fontSize: 14,
fontWeight: '600',
}}>
{personDetails.known_for_department}
</Text>
</View>
)}
<View style={{ {personDetails?.birthday && (
flexDirection: 'row', <View style={{
flexWrap: 'wrap', flexDirection: 'row',
marginBottom: 16, alignItems: 'center',
}}> marginBottom: personDetails?.place_of_birth ? 12 : 0
{personDetails?.known_for_department && ( }}>
<InfoBadge <MaterialIcons name="cake" size={16} color="#22C55E" />
label="Department" <Text style={{
value={personDetails.known_for_department} color: 'rgba(255, 255, 255, 0.7)',
icon="work" fontSize: 12,
color={currentTheme.colors.primary} marginLeft: 8,
bgColor={currentTheme.colors.primary + '15'} marginRight: 12,
/> }}>
)} Age
</Text>
{personDetails?.birthday && ( <Text style={{
<InfoBadge color: '#fff',
label="Age" fontSize: 14,
value={`${calculateAge(personDetails.birthday)} years old`} fontWeight: '600',
icon="cake" }}>
color="#22C55E" {calculateAge(personDetails.birthday)} years old
bgColor="rgba(34, 197, 94, 0.15)" </Text>
/> </View>
)} )}
{personDetails?.place_of_birth && ( {personDetails?.place_of_birth && (
<InfoBadge <View style={{ flexDirection: 'row', alignItems: 'center' }}>
label="Birth Place" <MaterialIcons name="place" size={16} color="#F59E0B" />
value={personDetails.place_of_birth} <Text style={{
icon="place" color: 'rgba(255, 255, 255, 0.7)',
color="#F59E0B" fontSize: 12,
bgColor="rgba(245, 158, 11, 0.15)" marginLeft: 8,
/> marginRight: 12,
)} }}>
</View> Born in
</Text>
<Text style={{
color: '#fff',
fontSize: 14,
fontWeight: '600',
flex: 1,
}}>
{personDetails.place_of_birth}
</Text>
</View>
)}
{personDetails?.birthday && ( {personDetails?.birthday && (
<View style={{ <View style={{
backgroundColor: 'rgba(255, 255, 255, 0.03)', marginTop: 12,
borderRadius: 16, paddingTop: 12,
padding: 16, borderTopWidth: 1,
borderWidth: 1, borderTopColor: 'rgba(255, 255, 255, 0.1)',
borderColor: 'rgba(255, 255, 255, 0.08)',
marginBottom: 16,
}}> }}>
<View style={{
flexDirection: 'row',
alignItems: 'center',
marginBottom: 8,
}}>
<MaterialIcons name="event" size={16} color="rgba(255, 255, 255, 0.7)" />
<Text style={{
color: 'rgba(255, 255, 255, 0.7)',
fontSize: 12,
fontWeight: '600',
marginLeft: 6,
textTransform: 'uppercase',
letterSpacing: 0.3,
}}>
Born
</Text>
</View>
<Text style={{ <Text style={{
color: '#fff', color: 'rgba(255, 255, 255, 0.7)',
fontSize: 16, fontSize: 12,
fontWeight: '600', marginBottom: 4,
}}> }}>
{formatDate(personDetails.birthday)} Born on {formatDate(personDetails.birthday)}
</Text> </Text>
</View> </View>
)} )}
</View> </View>
)}
{/* Biography */} {/* Biography */}
{personDetails?.biography && ( {personDetails?.biography && (
<View style={{ marginBottom: 24 }}> <View style={{ marginBottom: 20 }}>
<Text style={{ <Text style={{
color: 'rgba(255, 255, 255, 0.7)', color: '#fff',
fontSize: 14, fontSize: 16,
fontWeight: '600', fontWeight: '700',
letterSpacing: 0.3, marginBottom: 12,
marginBottom: 16,
textTransform: 'uppercase',
}}>
Biography
</Text>
<View style={{
backgroundColor: 'rgba(255, 255, 255, 0.03)',
borderRadius: 20,
padding: 20,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.08)',
}}>
<Text style={{
color: 'rgba(255, 255, 255, 0.9)',
fontSize: 14,
lineHeight: 22,
fontWeight: '400',
letterSpacing: 0.1,
}}>
{personDetails.biography}
</Text>
</View>
</View>
)}
{/* Also Known As */}
{personDetails?.also_known_as && personDetails.also_known_as.length > 0 && (
<View style={{ marginBottom: 24 }}>
<Text style={{
color: 'rgba(255, 255, 255, 0.7)',
fontSize: 14,
fontWeight: '600',
letterSpacing: 0.3,
marginBottom: 16,
textTransform: 'uppercase',
}}>
Also Known As
</Text>
<View style={{
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
}}>
{personDetails.also_known_as.slice(0, 6).map((alias, index) => (
<View
key={index}
style={{
backgroundColor: 'rgba(255, 255, 255, 0.05)',
borderRadius: 12,
paddingHorizontal: 12,
paddingVertical: 8,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.1)',
}}
>
<Text style={{
color: 'rgba(255, 255, 255, 0.8)',
fontSize: 12,
fontWeight: '500',
}}>
{alias}
</Text>
</View>
))}
</View>
</View>
)}
{/* No details available */}
{!loading && !personDetails?.biography && !personDetails?.birthday && !personDetails?.place_of_birth && (
<View style={{
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 40,
}}> }}>
<MaterialIcons name="info" size={48} color="rgba(255, 255, 255, 0.3)" /> Biography
<Text style={{ </Text>
color: 'rgba(255, 255, 255, 0.7)', <Text style={{
fontSize: 16, color: 'rgba(255, 255, 255, 0.9)',
marginTop: 16, fontSize: 14,
fontWeight: '500', lineHeight: 20,
textAlign: 'center', fontWeight: '400',
}}> }}>
No additional details available {personDetails.biography}
</Text> </Text>
</View> </View>
)} )}
</View>
)} {/* Also Known As - Compact */}
</ScrollView> {personDetails?.also_known_as && personDetails.also_known_as.length > 0 && (
</BlurView> <View>
</Animated.View> <Text style={{
</Animated.View> color: '#fff',
); fontSize: 16,
fontWeight: '700',
marginBottom: 12,
}}>
Also Known As
</Text>
<Text style={{
color: 'rgba(255, 255, 255, 0.8)',
fontSize: 14,
lineHeight: 20,
}}>
{personDetails.also_known_as.slice(0, 4).join(' • ')}
</Text>
</View>
)}
{/* No details available */}
{!loading && !personDetails?.biography && !personDetails?.birthday && !personDetails?.place_of_birth && (
<View style={{
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 40,
}}>
<MaterialIcons name="info" size={32} color="rgba(255, 255, 255, 0.3)" />
<Text style={{
color: 'rgba(255, 255, 255, 0.7)',
fontSize: 14,
marginTop: 12,
textAlign: 'center',
}}>
No additional details available
</Text>
</View>
)}
</View>
)}
</ScrollView>
</>
);
}
}; };
export default CastDetailsModal; export default CastDetailsModal;