mojmajstor/components/ui/toast.tsx
2026-06-06 03:54:22 +02:00

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>
);
}