94 lines
3.1 KiB
TypeScript
94 lines
3.1 KiB
TypeScript
import { useState, useEffect, useRef, useCallback } from "react";
|
|
import { Animated, Text, View, type ViewStyle } from "react-native";
|
|
import { Ionicons } from "@expo/vector-icons";
|
|
import { useTheme } from "../theme";
|
|
|
|
type ToastType = "success" | "error" | "info";
|
|
|
|
interface ToastData {
|
|
message: string;
|
|
type: ToastType;
|
|
id: number;
|
|
}
|
|
|
|
let toastId = 0;
|
|
let showToastFn: ((message: string, type?: ToastType) => void) | null = null;
|
|
|
|
export function showToast(message: string, type: ToastType = "info") {
|
|
showToastFn?.(message, type);
|
|
}
|
|
|
|
export function ToastProvider({ children }: { children: React.ReactNode }) {
|
|
const theme = useTheme();
|
|
const [toast, setToast] = useState<ToastData | null>(null);
|
|
const opacity = useRef(new Animated.Value(0)).current;
|
|
const translateY = useRef(new Animated.Value(-24)).current;
|
|
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
const show = useCallback((message: string, type: ToastType = "info") => {
|
|
if (timerRef.current) clearTimeout(timerRef.current);
|
|
setToast({ message, type, id: ++toastId });
|
|
Animated.parallel([
|
|
Animated.timing(opacity, { toValue: 1, duration: 220, useNativeDriver: true }),
|
|
Animated.timing(translateY, { toValue: 0, duration: 220, useNativeDriver: true }),
|
|
]).start();
|
|
timerRef.current = setTimeout(() => {
|
|
Animated.parallel([
|
|
Animated.timing(opacity, { toValue: 0, duration: 220, useNativeDriver: true }),
|
|
Animated.timing(translateY, { toValue: -24, duration: 220, useNativeDriver: true }),
|
|
]).start(() => setToast(null));
|
|
}, 3200);
|
|
}, [opacity, translateY]);
|
|
|
|
useEffect(() => {
|
|
showToastFn = show;
|
|
return () => { showToastFn = null; };
|
|
}, [show]);
|
|
|
|
const styles: Record<ToastType, { bg: string; icon: string; text: string }> = {
|
|
success: { bg: theme.colors.success, icon: "checkmark-circle", text: "#FFFFFF" },
|
|
error: { bg: theme.colors.error, icon: "alert-circle", text: "#FFFFFF" },
|
|
info: { bg: theme.colors.secondary, icon: "information-circle", text: "#FFFFFF" },
|
|
};
|
|
|
|
return (
|
|
<View style={{ flex: 1 }}>
|
|
{children}
|
|
{toast && (
|
|
<Animated.View
|
|
style={{
|
|
position: "absolute",
|
|
top: 56,
|
|
left: theme.spacing.lg,
|
|
right: theme.spacing.lg,
|
|
backgroundColor: styles[toast.type].bg,
|
|
borderRadius: theme.radius.lg,
|
|
paddingVertical: 14,
|
|
paddingHorizontal: theme.spacing.md,
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: theme.spacing.sm,
|
|
...theme.shadows.lg,
|
|
opacity,
|
|
transform: [{ translateY }],
|
|
zIndex: 9999,
|
|
} as ViewStyle}
|
|
>
|
|
<Ionicons name={styles[toast.type].icon as any} size={22} color={styles[toast.type].text} />
|
|
<Text
|
|
style={{
|
|
color: styles[toast.type].text,
|
|
fontSize: 14,
|
|
fontWeight: "500",
|
|
flex: 1,
|
|
}}
|
|
numberOfLines={2}
|
|
>
|
|
{toast.message}
|
|
</Text>
|
|
</Animated.View>
|
|
)}
|
|
</View>
|
|
);
|
|
}
|