174 lines
5.2 KiB
TypeScript
174 lines
5.2 KiB
TypeScript
import { useEffect, useRef } from 'react';
|
|
import { Modal, View, Text, Pressable, Animated, Easing } from 'react-native';
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
type Props = {
|
|
visible: boolean;
|
|
title: string;
|
|
message?: string;
|
|
onClose: () => void;
|
|
};
|
|
|
|
/**
|
|
* iOS-style success alert mit animiertem Check-Icon.
|
|
* - Card scaled mit Spring-overshoot rein
|
|
* - Check-Icon sequenced danach mit eigenem Spring + rotation-pop
|
|
* - Tap auf Backdrop schließt
|
|
* - OK-Button schließt
|
|
*/
|
|
export function SuccessAlert({ visible, title, message, onClose }: Props) {
|
|
const { t } = useTranslation();
|
|
const cardScale = useRef(new Animated.Value(0.8)).current;
|
|
const cardOpacity = useRef(new Animated.Value(0)).current;
|
|
const checkScale = useRef(new Animated.Value(0)).current;
|
|
const checkRotate = useRef(new Animated.Value(0)).current;
|
|
|
|
useEffect(() => {
|
|
if (visible) {
|
|
cardScale.setValue(0.8);
|
|
cardOpacity.setValue(0);
|
|
checkScale.setValue(0);
|
|
checkRotate.setValue(0);
|
|
|
|
Animated.parallel([
|
|
Animated.spring(cardScale, {
|
|
toValue: 1,
|
|
useNativeDriver: true,
|
|
friction: 7,
|
|
tension: 80,
|
|
}),
|
|
Animated.timing(cardOpacity, {
|
|
toValue: 1,
|
|
duration: 220,
|
|
useNativeDriver: true,
|
|
easing: Easing.out(Easing.cubic),
|
|
}),
|
|
Animated.sequence([
|
|
Animated.delay(140),
|
|
Animated.parallel([
|
|
Animated.spring(checkScale, {
|
|
toValue: 1,
|
|
useNativeDriver: true,
|
|
friction: 5,
|
|
tension: 180,
|
|
}),
|
|
Animated.timing(checkRotate, {
|
|
toValue: 1,
|
|
duration: 380,
|
|
useNativeDriver: true,
|
|
easing: Easing.out(Easing.back(1.7)),
|
|
}),
|
|
]),
|
|
]),
|
|
]).start();
|
|
}
|
|
}, [visible, cardScale, cardOpacity, checkScale, checkRotate]);
|
|
|
|
const rotateInterpolate = checkRotate.interpolate({
|
|
inputRange: [0, 1],
|
|
outputRange: ['-30deg', '0deg'],
|
|
});
|
|
|
|
return (
|
|
<Modal visible={visible} transparent animationType="fade" onRequestClose={onClose}>
|
|
{/* Backdrop — Pressable damit Tap-outside schließt */}
|
|
<Pressable
|
|
onPress={onClose}
|
|
style={{
|
|
flex: 1,
|
|
backgroundColor: 'rgba(0,0,0,0.4)',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
padding: 24,
|
|
}}
|
|
>
|
|
{/* Card — Pressable mit onPress={()=>{}} damit Tap auf Card NICHT bubbelt
|
|
* zum Backdrop und das Modal schließt. */}
|
|
<Pressable onPress={() => {}} style={{ width: '85%', maxWidth: 320 }}>
|
|
<Animated.View
|
|
style={{
|
|
backgroundColor: '#fff',
|
|
borderRadius: 22,
|
|
padding: 20,
|
|
width: '100%',
|
|
transform: [{ scale: cardScale }],
|
|
opacity: cardOpacity,
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 8 },
|
|
shadowOpacity: 0.2,
|
|
shadowRadius: 24,
|
|
elevation: 16,
|
|
}}
|
|
>
|
|
{/* Animated Check-Circle */}
|
|
<Animated.View
|
|
style={{
|
|
width: 56,
|
|
height: 56,
|
|
borderRadius: 28,
|
|
backgroundColor: '#16a34a',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
marginBottom: 12,
|
|
alignSelf: 'center',
|
|
transform: [{ scale: checkScale }, { rotate: rotateInterpolate }],
|
|
shadowColor: '#16a34a',
|
|
shadowOffset: { width: 0, height: 4 },
|
|
shadowOpacity: 0.4,
|
|
shadowRadius: 8,
|
|
elevation: 8,
|
|
}}
|
|
>
|
|
<Ionicons name="checkmark" size={32} color="#fff" />
|
|
</Animated.View>
|
|
|
|
<Text
|
|
style={{
|
|
fontSize: 16,
|
|
fontFamily: 'Nunito_700Bold',
|
|
color: '#0a0a0a',
|
|
textAlign: 'center',
|
|
marginBottom: 6,
|
|
}}
|
|
>
|
|
{title}
|
|
</Text>
|
|
{message && (
|
|
<Text
|
|
style={{
|
|
fontSize: 13,
|
|
fontFamily: 'Nunito_400Regular',
|
|
color: '#525252',
|
|
textAlign: 'center',
|
|
lineHeight: 19,
|
|
marginBottom: 14,
|
|
}}
|
|
>
|
|
{message}
|
|
</Text>
|
|
)}
|
|
|
|
<Pressable
|
|
onPress={onClose}
|
|
style={{
|
|
paddingVertical: 10,
|
|
borderRadius: 10,
|
|
backgroundColor: '#eff6ff',
|
|
borderWidth: 1,
|
|
borderColor: '#bfdbfe',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
}}
|
|
>
|
|
<Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: '#007AFF' }}>
|
|
{t('common.ok')}
|
|
</Text>
|
|
</Pressable>
|
|
</Animated.View>
|
|
</Pressable>
|
|
</Pressable>
|
|
</Modal>
|
|
);
|
|
}
|