entregas_app/components/FloatingPanicButton.tsx

861 lines
24 KiB
TypeScript
Raw Normal View History

"use client"
import type React from "react"
import { useState, useEffect, useRef } from "react"
import {
StyleSheet,
TouchableOpacity,
View,
Text,
Vibration,
ActivityIndicator,
Dimensions,
Animated,
Linking,
} from "react-native"
import { Ionicons } from "@expo/vector-icons"
import * as Location from "expo-location"
import AsyncStorage from "@react-native-async-storage/async-storage"
import { PanGestureHandler, State } from "react-native-gesture-handler"
import { LinearGradient } from "expo-linear-gradient"
import { Modalize } from "react-native-modalize"
import { COLORS, SHADOWS } from "../src/constants/theme"
import { StatusBar } from "expo-status-bar"
const STORAGE_KEY = "panic_button_position"
const { width, height } = Dimensions.get("window")
const BUTTON_SIZE = 56
interface EnhancedPanicButtonProps {
onPanic?: (location?: { latitude: number; longitude: number } | null) => void
}
const EnhancedPanicButton: React.FC<EnhancedPanicButtonProps> = ({ onPanic }) => {
const [loading, setLoading] = useState(false)
const [ready, setReady] = useState(false)
const [position, setPosition] = useState({ x: width - BUTTON_SIZE - 24, y: height - BUTTON_SIZE - 120 })
const [currentModal, setCurrentModal] = useState<'main' | 'permission' | 'error' | null>(null)
// Refs para os modais
const mainModalRef = useRef<Modalize>(null)
const permissionModalRef = useRef<Modalize>(null)
const errorModalRef = useRef<Modalize>(null)
// Animated values
const translateX = useRef(new Animated.Value(position.x)).current
const translateY = useRef(new Animated.Value(position.y)).current
const lastOffset = useRef({ x: position.x, y: position.y })
const pulseAnim = useRef(new Animated.Value(1)).current
const shakeAnim = useRef(new Animated.Value(0)).current
const glowAnim = useRef(new Animated.Value(0)).current
// Animação de pulso e brilho do botão
useEffect(() => {
const pulse = Animated.loop(
Animated.sequence([
Animated.timing(pulseAnim, {
toValue: 1.12,
duration: 1200,
useNativeDriver: true,
}),
Animated.timing(pulseAnim, {
toValue: 1,
duration: 1200,
useNativeDriver: true,
}),
]),
)
const glow = Animated.loop(
Animated.sequence([
Animated.timing(glowAnim, {
toValue: 1,
duration: 2000,
useNativeDriver: true,
}),
Animated.timing(glowAnim, {
toValue: 0,
duration: 2000,
useNativeDriver: true,
}),
]),
)
pulse.start()
glow.start()
return () => {
pulse.stop()
glow.stop()
}
}, [])
// Carregar posição salva
useEffect(() => {
AsyncStorage.getItem(STORAGE_KEY).then((data) => {
if (data) {
const pos = JSON.parse(data)
setPosition(pos)
translateX.setValue(pos.x)
translateY.setValue(pos.y)
lastOffset.current = pos
}
setReady(true)
})
}, [])
// Salvar posição ao mover
const savePosition = (x: number, y: number) => {
const newPos = {
x: Math.max(0, Math.min(x, width - BUTTON_SIZE)),
y: Math.max(0, Math.min(y, height - BUTTON_SIZE)),
}
setPosition(newPos)
AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(newPos))
lastOffset.current = newPos
}
const clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(value, max))
const onHandlerStateChange = (event: any) => {
if (event.nativeEvent.oldState === State.ACTIVE) {
let newX = lastOffset.current.x + event.nativeEvent.translationX
let newY = lastOffset.current.y + event.nativeEvent.translationY
newX = clamp(newX, 0, width - BUTTON_SIZE)
newY = clamp(newY, 0, height - BUTTON_SIZE)
savePosition(newX, newY)
translateX.setValue(newX)
translateY.setValue(newY)
translateX.setOffset(0)
translateY.setOffset(0)
}
}
const handlePress = () => {
Vibration.vibrate([50, 100, 50])
setCurrentModal('main')
mainModalRef.current?.open()
}
const shakeAnimation = () => {
Animated.sequence([
Animated.timing(shakeAnim, { toValue: 10, duration: 80, useNativeDriver: true }),
Animated.timing(shakeAnim, { toValue: -10, duration: 80, useNativeDriver: true }),
Animated.timing(shakeAnim, { toValue: 10, duration: 80, useNativeDriver: true }),
Animated.timing(shakeAnim, { toValue: -10, duration: 80, useNativeDriver: true }),
Animated.timing(shakeAnim, { toValue: 0, duration: 80, useNativeDriver: true }),
]).start()
}
const confirmPanic = async () => {
setLoading(true)
try {
const { status } = await Location.requestForegroundPermissionsAsync()
if (status !== "granted") {
setLoading(false)
mainModalRef.current?.close()
setTimeout(() => {
setCurrentModal('permission')
permissionModalRef.current?.open()
}, 300)
shakeAnimation()
return
}
const location = await Location.getCurrentPositionAsync({
accuracy: Location.Accuracy.High,
timeout: 10000,
})
setLoading(false)
mainModalRef.current?.close()
Vibration.vibrate([100, 200, 100, 200, 100])
if (onPanic) {
onPanic({
latitude: location.coords.latitude,
longitude: location.coords.longitude,
})
}
} catch (e) {
setLoading(false)
mainModalRef.current?.close()
setTimeout(() => {
setCurrentModal('error')
errorModalRef.current?.open()
}, 300)
shakeAnimation()
}
}
const handlePermissionDenied = () => {
permissionModalRef.current?.close()
if (onPanic) onPanic(null)
}
const handleLocationError = () => {
errorModalRef.current?.close()
if (onPanic) onPanic(null)
}
const openSettings = () => {
Linking.openSettings()
permissionModalRef.current?.close()
}
const retryLocation = () => {
errorModalRef.current?.close()
setTimeout(() => {
setCurrentModal('main')
mainModalRef.current?.open()
}, 300)
}
if (!ready) return null
const renderMainModal = () => (
<View style={styles.modalContent}>
{/* Header com gradiente */}
<LinearGradient
colors={["#DC2626", "#EF4444", "#F87171"]}
style={styles.modalHeader}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
>
<View style={styles.headerContent}>
<View style={styles.iconContainer}>
<View style={styles.iconRing}>
<Ionicons name="alert" size={32} color="#fff" />
</View>
</View>
<Text style={styles.modalTitle}>Botão de Pânico</Text>
<Text style={styles.modalSubtitle}>Situação de emergência detectada</Text>
</View>
</LinearGradient>
{/* Corpo do modal */}
<View style={styles.modalBody}>
<View style={styles.warningSection}>
<LinearGradient
colors={["#FEF3C7", "#FEF9E7"]}
style={styles.warningCard}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
>
<View style={styles.warningIconContainer}>
<Ionicons name="warning" size={20} color="#D97706" />
</View>
<Text style={styles.warningText}>Use apenas em emergências reais</Text>
</LinearGradient>
</View>
<View style={styles.featuresSection}>
<Text style={styles.featuresTitle}>O que acontecerá:</Text>
<View style={styles.featuresList}>
<View style={styles.featureItem}>
<LinearGradient
colors={[COLORS.primary, "#3B82F6"]}
style={styles.featureIcon}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
>
<Ionicons name="location" size={16} color="white" />
</LinearGradient>
<View style={styles.featureContent}>
<Text style={styles.featureTitle}>Localização Enviada</Text>
<Text style={styles.featureDescription}>Sua posição GPS será compartilhada</Text>
</View>
</View>
<View style={styles.featureItem}>
<LinearGradient
colors={["#10B981", "#059669"]}
style={styles.featureIcon}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
>
<Ionicons name="call" size={16} color="white" />
</LinearGradient>
<View style={styles.featureContent}>
<Text style={styles.featureTitle}>Equipe Notificada</Text>
<Text style={styles.featureDescription}>Emergência será comunicada imediatamente</Text>
</View>
</View>
<View style={styles.featureItem}>
<LinearGradient
colors={["#8B5CF6", "#7C3AED"]}
style={styles.featureIcon}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
>
<Ionicons name="flash" size={16} color="white" />
</LinearGradient>
<View style={styles.featureContent}>
<Text style={styles.featureTitle}>Resposta Rápida</Text>
<Text style={styles.featureDescription}>Ação imediata será iniciada</Text>
</View>
</View>
</View>
</View>
{loading ? (
<View style={styles.loadingSection}>
<LinearGradient
colors={["#FEF2F2", "#FFFFFF"]}
style={styles.loadingContainer}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
>
<ActivityIndicator size="large" color="#EF4444" />
<Text style={styles.loadingText}>Obtendo sua localização...</Text>
<Text style={styles.loadingSubtext}>Aguarde alguns segundos</Text>
</LinearGradient>
</View>
) : (
<View style={styles.actionsSection}>
<TouchableOpacity
style={styles.cancelButton}
onPress={() => mainModalRef.current?.close()}
>
<Text style={styles.cancelButtonText}>Cancelar</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.confirmButton} onPress={confirmPanic}>
<LinearGradient
colors={["#DC2626", "#EF4444"]}
style={styles.confirmButtonGradient}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
>
<Ionicons name="alert-circle" size={20} color="white" />
<Text style={styles.confirmButtonText}>Acionar Pânico</Text>
</LinearGradient>
</TouchableOpacity>
</View>
)}
</View>
</View>
)
const renderPermissionModal = () => (
<View style={styles.modalContent}>
<LinearGradient
colors={["#D97706", "#F59E0B", "#FBBF24"]}
style={styles.modalHeader}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
>
<View style={styles.headerContent}>
<View style={styles.iconContainer}>
<View style={styles.iconRing}>
<Ionicons name="location-off" size={32} color="#fff" />
</View>
</View>
<Text style={styles.modalTitle}>Permissão Necessária</Text>
<Text style={styles.modalSubtitle}>Acesso à localização requerido</Text>
</View>
</LinearGradient>
<View style={styles.modalBody}>
<View style={styles.permissionSection}>
<LinearGradient
colors={["#FEF3C7", "#FFFBEB"]}
style={styles.permissionCard}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
>
<Ionicons name="shield-checkmark" size={24} color="#D97706" />
<Text style={styles.permissionTitle}>Por que precisamos?</Text>
<Text style={styles.permissionText}>
Para enviar sua localização exata à equipe de emergência e garantir que o socorro chegue rapidamente ao local correto.
</Text>
</LinearGradient>
</View>
<View style={styles.actionsSection}>
<TouchableOpacity style={styles.cancelButton} onPress={handlePermissionDenied}>
<Text style={styles.cancelButtonText}>Continuar sem GPS</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.settingsButton} onPress={openSettings}>
<LinearGradient
colors={["#D97706", "#F59E0B"]}
style={styles.confirmButtonGradient}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
>
<Ionicons name="settings" size={20} color="white" />
<Text style={styles.confirmButtonText}>Abrir Configurações</Text>
</LinearGradient>
</TouchableOpacity>
</View>
</View>
</View>
)
const renderErrorModal = () => (
<View style={styles.modalContent}>
<LinearGradient
colors={["#DC2626", "#EF4444", "#F87171"]}
style={styles.modalHeader}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
>
<View style={styles.headerContent}>
<View style={styles.iconContainer}>
<View style={styles.iconRing}>
<Ionicons name="alert-circle" size={32} color="#fff" />
</View>
</View>
<Text style={styles.modalTitle}>Erro de Localização</Text>
<Text style={styles.modalSubtitle}>Não foi possível obter GPS</Text>
</View>
</LinearGradient>
<View style={styles.modalBody}>
<View style={styles.errorSection}>
<LinearGradient
colors={["#FEF2F2", "#FFFFFF"]}
style={styles.errorCard}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
>
<Ionicons name="information-circle" size={24} color="#EF4444" />
<Text style={styles.errorTitle}>O que fazer?</Text>
<Text style={styles.errorText}>
Verifique se o GPS está ativado{'\n'}
Certifique-se de estar em local aberto{'\n'}
O pânico será acionado mesmo sem localização
</Text>
</LinearGradient>
</View>
<View style={styles.actionsSection}>
<TouchableOpacity style={styles.cancelButton} onPress={handleLocationError}>
<Text style={styles.cancelButtonText}>Acionar sem GPS</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.retryButton} onPress={retryLocation}>
<LinearGradient
colors={["#059669", "#10B981"]}
style={styles.confirmButtonGradient}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
>
<Ionicons name="refresh" size={20} color="white" />
<Text style={styles.confirmButtonText}>Tentar Novamente</Text>
</LinearGradient>
</TouchableOpacity>
</View>
</View>
</View>
)
return (
<>
<StatusBar style="dark" backgroundColor="transparent" translucent />
<PanGestureHandler
onGestureEvent={(event) => {
const { translationX, translationY } = event.nativeEvent
let newX = lastOffset.current.x + translationX
let newY = lastOffset.current.y + translationY
newX = clamp(newX, 0, width - BUTTON_SIZE)
newY = clamp(newY, 0, height - BUTTON_SIZE)
translateX.setValue(newX)
translateY.setValue(newY)
}}
onHandlerStateChange={onHandlerStateChange}
>
<Animated.View
style={[
styles.fab,
{
transform: [
{ translateX },
{ translateY },
{ scale: pulseAnim },
{ translateX: shakeAnim }
],
},
]}
>
{/* Efeito de brilho */}
<Animated.View
style={[
styles.glowEffect,
{
opacity: glowAnim.interpolate({
inputRange: [0, 1],
outputRange: [0.3, 0.8],
}),
transform: [
{
scale: glowAnim.interpolate({
inputRange: [0, 1],
outputRange: [1, 1.3],
}),
},
],
},
]}
/>
<TouchableOpacity onPress={handlePress} activeOpacity={0.8} style={styles.fabButton}>
<LinearGradient
colors={["#DC2626", "#EF4444", "#F87171"]}
style={styles.fabGradient}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
>
<View style={styles.fabIconContainer}>
<Ionicons name="alert" size={24} color="#fff" />
</View>
</LinearGradient>
</TouchableOpacity>
</Animated.View>
</PanGestureHandler>
{/* Modais sem Portal */}
<Modalize
ref={mainModalRef}
adjustToContentHeight
handlePosition="inside"
handleStyle={styles.modalHandle}
modalStyle={styles.modalStyle}
overlayStyle={styles.overlayStyle}
childrenStyle={styles.childrenStyle}
onClosed={() => setCurrentModal(null)}
>
{renderMainModal()}
</Modalize>
<Modalize
ref={permissionModalRef}
adjustToContentHeight
handlePosition="inside"
handleStyle={styles.modalHandle}
modalStyle={styles.modalStyle}
overlayStyle={styles.overlayStyle}
childrenStyle={styles.childrenStyle}
onClosed={() => setCurrentModal(null)}
>
{renderPermissionModal()}
</Modalize>
<Modalize
ref={errorModalRef}
adjustToContentHeight
handlePosition="inside"
handleStyle={styles.modalHandle}
modalStyle={styles.modalStyle}
overlayStyle={styles.overlayStyle}
childrenStyle={styles.childrenStyle}
onClosed={() => setCurrentModal(null)}
>
{renderErrorModal()}
</Modalize>
</>
)
}
const styles = StyleSheet.create({
fab: {
position: "absolute",
width: BUTTON_SIZE,
height: BUTTON_SIZE,
borderRadius: BUTTON_SIZE / 2,
zIndex: 999,
},
glowEffect: {
position: "absolute",
width: BUTTON_SIZE + 16,
height: BUTTON_SIZE + 16,
borderRadius: (BUTTON_SIZE + 16) / 2,
backgroundColor: "#EF4444",
top: -8,
left: -8,
zIndex: -1,
},
fabButton: {
width: "100%",
height: "100%",
borderRadius: BUTTON_SIZE / 2,
overflow: "hidden",
...SHADOWS.large,
},
fabGradient: {
width: "100%",
height: "100%",
alignItems: "center",
justifyContent: "center",
borderRadius: BUTTON_SIZE / 2,
},
fabIconContainer: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: "rgba(255, 255, 255, 0.2)",
alignItems: "center",
justifyContent: "center",
},
// Estilos do Modalize
modalStyle: {
backgroundColor: "transparent",
},
overlayStyle: {
backgroundColor: "rgba(0, 0, 0, 0.6)",
},
childrenStyle: {
backgroundColor: "transparent",
},
modalHandle: {
backgroundColor: "rgba(255, 255, 255, 0.3)",
width: 40,
height: 4,
},
// Conteúdo dos modais
modalContent: {
backgroundColor: COLORS.card,
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
overflow: "hidden",
marginBottom: 0,
},
modalHeader: {
paddingTop: 32,
paddingBottom: 24,
paddingHorizontal: 24,
},
headerContent: {
alignItems: "center",
},
iconContainer: {
marginBottom: 16,
},
iconRing: {
width: 72,
height: 72,
borderRadius: 36,
backgroundColor: "rgba(255, 255, 255, 0.2)",
alignItems: "center",
justifyContent: "center",
borderWidth: 2,
borderColor: "rgba(255, 255, 255, 0.3)",
},
modalTitle: {
fontSize: 24,
fontWeight: "bold",
color: "white",
marginBottom: 8,
textAlign: "center",
},
modalSubtitle: {
fontSize: 16,
color: "rgba(255, 255, 255, 0.9)",
textAlign: "center",
lineHeight: 22,
},
modalBody: {
padding: 24,
},
// Seção de aviso
warningSection: {
marginBottom: 24,
},
warningCard: {
flexDirection: "row",
alignItems: "center",
padding: 16,
borderRadius: 16,
borderWidth: 1,
borderColor: "#F3E8FF",
},
warningIconContainer: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: "rgba(217, 119, 6, 0.1)",
alignItems: "center",
justifyContent: "center",
marginRight: 12,
},
warningText: {
fontSize: 14,
color: "#92400E",
fontWeight: "600",
flex: 1,
},
// Seção de recursos
featuresSection: {
marginBottom: 24,
},
featuresTitle: {
fontSize: 16,
fontWeight: "bold",
color: COLORS.text,
marginBottom: 16,
},
featuresList: {
gap: 12,
},
featureItem: {
flexDirection: "row",
alignItems: "center",
},
featureIcon: {
width: 32,
height: 32,
borderRadius: 16,
alignItems: "center",
justifyContent: "center",
marginRight: 12,
},
featureContent: {
flex: 1,
},
featureTitle: {
fontSize: 14,
fontWeight: "600",
color: COLORS.text,
marginBottom: 2,
},
featureDescription: {
fontSize: 12,
color: COLORS.textLight,
lineHeight: 16,
},
// Seção de loading
loadingSection: {
marginBottom: 8,
},
loadingContainer: {
alignItems: "center",
paddingVertical: 32,
paddingHorizontal: 24,
borderRadius: 16,
borderWidth: 1,
borderColor: "#FEE2E2",
},
loadingText: {
fontSize: 16,
color: COLORS.text,
marginTop: 16,
fontWeight: "600",
},
loadingSubtext: {
fontSize: 14,
color: COLORS.textLight,
marginTop: 4,
},
// Seções específicas dos modais
permissionSection: {
marginBottom: 24,
},
permissionCard: {
alignItems: "center",
padding: 24,
borderRadius: 16,
borderWidth: 1,
borderColor: "#FEF3C7",
},
permissionTitle: {
fontSize: 16,
fontWeight: "bold",
color: "#92400E",
marginTop: 12,
marginBottom: 8,
},
permissionText: {
fontSize: 14,
color: "#92400E",
textAlign: "center",
lineHeight: 20,
},
errorSection: {
marginBottom: 24,
},
errorCard: {
alignItems: "center",
padding: 24,
borderRadius: 16,
borderWidth: 1,
borderColor: "#FEE2E2",
},
errorTitle: {
fontSize: 16,
fontWeight: "bold",
color: "#DC2626",
marginTop: 12,
marginBottom: 8,
},
errorText: {
fontSize: 14,
color: "#DC2626",
textAlign: "center",
lineHeight: 20,
},
// Ações
actionsSection: {
flexDirection: "row",
gap: 12,
marginBottom: 20,
},
cancelButton: {
flex: 1,
backgroundColor: COLORS.background,
borderRadius: 16,
paddingVertical: 16,
alignItems: "center",
borderWidth: 1.5,
borderColor: COLORS.border,
},
cancelButtonText: {
fontSize: 16,
color: COLORS.textLight,
fontWeight: "600",
},
confirmButton: {
flex: 1,
borderRadius: 16,
overflow: "hidden",
...SHADOWS.medium,
},
settingsButton: {
flex: 1,
borderRadius: 16,
overflow: "hidden",
...SHADOWS.medium,
},
retryButton: {
flex: 1,
borderRadius: 16,
overflow: "hidden",
...SHADOWS.medium,
},
confirmButtonGradient: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
paddingVertical: 16,
gap: 8,
},
confirmButtonText: {
fontSize: 16,
color: "white",
fontWeight: "bold",
},
})
export default EnhancedPanicButton