chahinebrini 14452b2a46 refactor(native): Pressable → TouchableOpacity sweep (style-fn swallows Android styles)
Alle <Pressable style={({pressed}) => ({...})}> ersetzt — style-Funktion
droppt auf Android (New Arch) intermittierend width/height, führt zu 0×0
unsichtbaren Elementen. TouchableOpacity mit activeOpacity ist stabil.

Außerdem übrige Pressables (plain style) aus components/ und app/
migriert sowie zwei überschüssige </View>-Tags in chat.tsx + RoomCard.tsx
entfernt die TS-Fehler verursacht haben.

64 Dateien, typecheck sauber.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 15:43:10 +02:00

208 lines
6.5 KiB
TypeScript

import { useEffect, useRef } from 'react';
import { Modal, View, Text, TouchableOpacity, Animated, Easing } from 'react-native';
// Wichtig (UX-Entscheidung 2026-05-05): Icon im Confirm-Modal NICHT animieren —
// User sieht zwei Modals nacheinander (Confirm → Success), beide animierte Icons
// = visuelle Doppel-Eskalation, wirkt verwirrend. Daher: Card animiert auf,
// Icon erscheint statisch (kein scale-pop). Nur das nachfolgende SuccessAlert
// behält seine Icon-Animation als "Belohnungs-Moment".
import { Ionicons } from '@expo/vector-icons';
import { useTranslation } from 'react-i18next';
type Props = {
visible: boolean;
title: string;
message?: string;
/** Default: i18n "common.cancel" */
cancelLabel?: string;
/** Default: i18n "common.confirm" */
confirmLabel?: string;
/** Wenn true: Confirm-Button rot statt blau (für destructive Actions). */
destructive?: boolean;
/** Icon im Top-Circle. Default: question-mark. */
icon?: React.ComponentProps<typeof Ionicons>['name'];
/** Icon-Circle-Color. Default: #007AFF (iOS-blue). */
iconColor?: string;
onConfirm: () => void;
onCancel: () => void;
};
/**
* Animiertes iOS-style Confirm-Modal — gleicher Animations-Stil wie SuccessAlert,
* aber mit zwei Buttons (Cancel + Confirm). Tap-outside cancelt.
*/
export function ConfirmAlert({
visible,
title,
message,
cancelLabel,
confirmLabel,
destructive = false,
icon = 'help-circle',
iconColor = '#007AFF',
onConfirm,
onCancel,
}: Props) {
const { t } = useTranslation();
const resolvedCancelLabel = cancelLabel ?? t('common.cancel');
const resolvedConfirmLabel = confirmLabel ?? t('common.confirm');
const cardScale = useRef(new Animated.Value(0.8)).current;
const cardOpacity = useRef(new Animated.Value(0)).current;
useEffect(() => {
if (visible) {
cardScale.setValue(0.8);
cardOpacity.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),
}),
]).start();
}
}, [visible, cardScale, cardOpacity]);
const confirmBg = destructive ? '#FF3B30' : '#007AFF';
return (
<Modal visible={visible} transparent animationType="fade" onRequestClose={onCancel}>
<TouchableOpacity
activeOpacity={1}
onPress={onCancel}
style={{
flex: 1,
backgroundColor: 'rgba(0,0,0,0.4)',
justifyContent: 'center',
alignItems: 'center',
padding: 24,
}}
>
<TouchableOpacity activeOpacity={1} onPress={() => {}} style={{ width: '85%', maxWidth: 340 }}>
<Animated.View
style={{
backgroundColor: '#fff',
borderRadius: 22,
padding: 22,
width: '100%',
transform: [{ scale: cardScale }],
opacity: cardOpacity,
shadowColor: '#000',
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.2,
shadowRadius: 24,
elevation: 16,
}}
>
{/* Icon-Circle — statisch (keine Pop-Animation, siehe Header-Comment). */}
<View
style={{
width: 56,
height: 56,
borderRadius: 28,
backgroundColor: iconColor,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 14,
alignSelf: 'center',
shadowColor: iconColor,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.4,
shadowRadius: 8,
elevation: 6,
}}
>
<Ionicons name={icon} size={32} color="#fff" />
</View>
<Text
style={{
fontSize: 17,
fontFamily: 'Nunito_700Bold',
color: '#0a0a0a',
textAlign: 'center',
marginBottom: 8,
}}
>
{title}
</Text>
{message && (
<Text
style={{
fontSize: 14,
fontFamily: 'Nunito_400Regular',
color: '#525252',
textAlign: 'center',
lineHeight: 20,
marginBottom: 18,
}}
>
{message}
</Text>
)}
{/* Two buttons row */}
<View style={{ flexDirection: 'row' }}>
<View style={{ flex: 1, marginRight: 5 }}>
<TouchableOpacity
activeOpacity={0.7}
onPress={onCancel}
style={{
paddingVertical: 10,
borderRadius: 10,
backgroundColor: '#f5f5f5',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Text
style={{
fontSize: 14,
fontFamily: 'Nunito_600SemiBold',
color: '#0a0a0a',
}}
>
{resolvedCancelLabel}
</Text>
</TouchableOpacity>
</View>
<View style={{ flex: 1, marginLeft: 5 }}>
<TouchableOpacity
activeOpacity={0.7}
onPress={onConfirm}
style={{
paddingVertical: 10,
borderRadius: 10,
backgroundColor: destructive ? '#fef2f2' : '#eff6ff',
borderWidth: 1,
borderColor: destructive ? '#fecaca' : '#bfdbfe',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Text
style={{
fontSize: 14,
fontFamily: 'Nunito_700Bold',
color: confirmBg,
}}
>
{resolvedConfirmLabel}
</Text>
</TouchableOpacity>
</View>
</View>
</Animated.View>
</TouchableOpacity>
</TouchableOpacity>
</Modal>
);
}