First commit of the new app
This commit is contained in:
151
assets/components/EmergencyButton.tsx
Normal file
151
assets/components/EmergencyButton.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
import { useAuth } from '@/assets/contexts/useAuth';
|
||||
import { getCachedContacts } from '@/assets/services/offlineStorage';
|
||||
import { colors } from '@/assets/styles/colors';
|
||||
import { fonts } from '@/assets/styles/fonts';
|
||||
import { FontAwesome } from '@expo/vector-icons';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Alert, Linking, Platform, StyleSheet, Text, useWindowDimensions } from 'react-native';
|
||||
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
||||
import Animated, {
|
||||
runOnJS,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withSpring,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
const BTN_SIZE = 64;
|
||||
const MARGIN = 16;
|
||||
|
||||
export function EmergencyButton() {
|
||||
const { isAuthenticated } = useAuth();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { width: screenWidth, height: screenHeight } = useWindowDimensions();
|
||||
const [emergencyPhone, setEmergencyPhone] = useState<string | null>(null);
|
||||
|
||||
const tabBarHeight = (Platform.OS === 'ios' ? 54 : 64) + insets.bottom;
|
||||
const maxX = screenWidth - BTN_SIZE - MARGIN;
|
||||
const minY = insets.top + MARGIN;
|
||||
const maxY = screenHeight - tabBarHeight - BTN_SIZE - MARGIN;
|
||||
|
||||
const x = useSharedValue(screenWidth - BTN_SIZE - MARGIN);
|
||||
const y = useSharedValue(screenHeight - tabBarHeight - BTN_SIZE - MARGIN);
|
||||
const startX = useSharedValue(0);
|
||||
const startY = useSharedValue(0);
|
||||
const scale = useSharedValue(1);
|
||||
const isDragActive = useSharedValue(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated) return;
|
||||
getCachedContacts().then(({ contactData }) => {
|
||||
if (contactData?.emergencyPhone) {
|
||||
setEmergencyPhone(contactData.emergencyPhone);
|
||||
}
|
||||
});
|
||||
}, [isAuthenticated]);
|
||||
|
||||
const handlePress = () => {
|
||||
if (!emergencyPhone) return;
|
||||
const url = `tel:${emergencyPhone.replace(/\s/g, '')}`;
|
||||
Alert.alert(
|
||||
'Linha de Emergência 24h',
|
||||
`Ligar para ${emergencyPhone}?`,
|
||||
[
|
||||
{ text: 'Cancelar', style: 'cancel' },
|
||||
{
|
||||
text: 'Ligar',
|
||||
style: 'default',
|
||||
onPress: () =>
|
||||
Linking.openURL(url).catch(() =>
|
||||
Alert.alert('Erro', 'Não foi possível abrir a aplicação de chamadas.'),
|
||||
),
|
||||
},
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
// Toque simples → abre o Alert
|
||||
const tap = Gesture.Tap()
|
||||
.maxDuration(500)
|
||||
.onEnd(() => {
|
||||
runOnJS(handlePress)();
|
||||
});
|
||||
|
||||
// Segurar 800ms → ativa modo arrasto com feedback de escala
|
||||
const longPress = Gesture.LongPress()
|
||||
.minDuration(800)
|
||||
.onStart(() => {
|
||||
isDragActive.value = true;
|
||||
scale.value = withSpring(1.2, { damping: 12, stiffness: 200 });
|
||||
startX.value = x.value;
|
||||
startY.value = y.value;
|
||||
});
|
||||
|
||||
// Pan → só move quando o modo arrasto está ativo
|
||||
const pan = Gesture.Pan()
|
||||
.onUpdate((e) => {
|
||||
if (!isDragActive.value) return;
|
||||
x.value = Math.max(MARGIN, Math.min(maxX, startX.value + e.translationX));
|
||||
y.value = Math.max(minY, Math.min(maxY, startY.value + e.translationY));
|
||||
})
|
||||
.onEnd(() => {
|
||||
if (!isDragActive.value) return;
|
||||
isDragActive.value = false;
|
||||
scale.value = withSpring(1, { damping: 15, stiffness: 250 });
|
||||
// snap para a borda mais próxima sem bounce
|
||||
const snapRight = x.value + BTN_SIZE / 2 > screenWidth / 2;
|
||||
x.value = withTiming(snapRight ? maxX : MARGIN, { duration: 250 });
|
||||
});
|
||||
|
||||
const composed = Gesture.Race(
|
||||
tap,
|
||||
Gesture.Simultaneous(longPress, pan),
|
||||
);
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [
|
||||
{ translateX: x.value },
|
||||
{ translateY: y.value },
|
||||
{ scale: scale.value },
|
||||
],
|
||||
}));
|
||||
|
||||
if (!isAuthenticated || !emergencyPhone) return null;
|
||||
|
||||
return (
|
||||
<GestureDetector gesture={composed}>
|
||||
<Animated.View style={[styles.btn, animatedStyle]}>
|
||||
<FontAwesome name="phone" size={18} color={colors.branco} />
|
||||
<Text style={styles.label}>SOS</Text>
|
||||
</Animated.View>
|
||||
</GestureDetector>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
btn: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: BTN_SIZE,
|
||||
height: BTN_SIZE,
|
||||
borderRadius: BTN_SIZE / 2,
|
||||
backgroundColor: colors.vermelho,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 4,
|
||||
shadowColor: '#000',
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 8,
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
elevation: 10,
|
||||
zIndex: 999,
|
||||
},
|
||||
label: {
|
||||
color: colors.branco,
|
||||
fontFamily: fonts.bold,
|
||||
fontSize: 11,
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user