feat: add notification push
This commit is contained in:
121
frontend/src/components/NotificationToast.tsx
Normal file
121
frontend/src/components/NotificationToast.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* NotificationToast.tsx
|
||||
*
|
||||
* Banner animado que cae desde arriba cuando llega una notificación
|
||||
* nueva del backend. Se autodescarta a los 5 segundos.
|
||||
*
|
||||
* Limitación: solo se muestra cuando la app está en primer plano. Para
|
||||
* notificaciones del SO con la app cerrada se necesita un Development
|
||||
* Build + expo-notifications (Expo Go SDK 53+ no lo soporta).
|
||||
*/
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import {
|
||||
Animated,
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
Platform,
|
||||
} from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
|
||||
import { useApp } from "../context/AppContext";
|
||||
|
||||
const AUTO_DISMISS_MS = 5000;
|
||||
|
||||
export default function NotificationToast() {
|
||||
const { toast, dismissToast } = useApp();
|
||||
const translateY = useRef(new Animated.Value(-180)).current;
|
||||
|
||||
useEffect(() => {
|
||||
if (!toast) return;
|
||||
|
||||
// Slide in
|
||||
Animated.spring(translateY, {
|
||||
toValue: 0,
|
||||
useNativeDriver: true,
|
||||
damping: 18,
|
||||
}).start();
|
||||
|
||||
// Auto dismiss después de N segundos
|
||||
const timer = setTimeout(() => {
|
||||
Animated.timing(translateY, {
|
||||
toValue: -180,
|
||||
duration: 250,
|
||||
useNativeDriver: true,
|
||||
}).start(() => dismissToast());
|
||||
}, AUTO_DISMISS_MS);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [toast, translateY, dismissToast]);
|
||||
|
||||
if (!toast) return null;
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.container,
|
||||
{ transform: [{ translateY }] },
|
||||
]}
|
||||
pointerEvents="box-none"
|
||||
>
|
||||
<Pressable style={styles.card} onPress={dismissToast}>
|
||||
<View style={styles.iconWrap}>
|
||||
<Ionicons name="notifications" size={22} color="#FFFFFF" />
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={styles.title} numberOfLines={1}>
|
||||
{toast.title}
|
||||
</Text>
|
||||
<Text style={styles.body} numberOfLines={2}>
|
||||
{toast.body}
|
||||
</Text>
|
||||
</View>
|
||||
<Ionicons name="close" size={18} color="#FFFFFF" />
|
||||
</Pressable>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
position: "absolute",
|
||||
top: Platform.OS === "ios" ? 50 : 30,
|
||||
left: 16,
|
||||
right: 16,
|
||||
zIndex: 9999,
|
||||
},
|
||||
card: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
backgroundColor: "#0E8A61",
|
||||
borderRadius: 14,
|
||||
padding: 14,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 6 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 10,
|
||||
elevation: 10,
|
||||
},
|
||||
iconWrap: {
|
||||
width: 38,
|
||||
height: 38,
|
||||
borderRadius: 19,
|
||||
backgroundColor: "rgba(255,255,255,0.2)",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginRight: 12,
|
||||
},
|
||||
title: {
|
||||
fontSize: 14,
|
||||
fontWeight: "800",
|
||||
color: "#FFFFFF",
|
||||
marginBottom: 2,
|
||||
},
|
||||
body: {
|
||||
fontSize: 12,
|
||||
color: "rgba(255,255,255,0.95)",
|
||||
lineHeight: 16,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user