Yaltopia-Tickets-App/components/Toast.tsx
2026-03-11 22:48:53 +03:00

147 lines
3.7 KiB
TypeScript

import React, { useEffect } from "react";
import { View, StyleSheet, Dimensions } from "react-native";
import { Text } from "@/components/ui/text";
import { useToast, ToastType } from "@/lib/toast-store";
import {
CheckCircle2,
AlertCircle,
AlertTriangle,
Lightbulb,
} from "@/lib/icons";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
withTiming,
runOnJS,
} from "react-native-reanimated";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
const { width: SCREEN_WIDTH } = Dimensions.get("window");
const SWIPE_THRESHOLD = SCREEN_WIDTH * 0.4;
const TOAST_VARIANTS: Record<
ToastType,
{
bg: string;
border: string;
icon: React.ReactNode;
}
> = {
success: {
bg: "rgba(34, 197, 94, 0.05)",
border: "#22c55e",
icon: <CheckCircle2 size={24} color="#22c55e" />,
},
info: {
bg: "rgba(14, 165, 233, 0.05)",
border: "#0ea5e9",
icon: <Lightbulb size={24} color="#0ea5e9" />,
},
warning: {
bg: "rgba(245, 158, 11, 0.05)",
border: "#f59e0b",
icon: <AlertTriangle size={24} color="#f59e0b" />,
},
error: {
bg: "rgba(239, 68, 68, 0.05)",
border: "#ef4444",
icon: <AlertCircle size={24} color="#ef4444" />,
},
};
export function Toast() {
const { visible, type, title, message, hide, duration } = useToast();
const insets = useSafeAreaInsets();
const opacity = useSharedValue(0);
const translateY = useSharedValue(-100);
const translateX = useSharedValue(0);
useEffect(() => {
if (visible) {
opacity.value = withTiming(1, { duration: 300 });
translateY.value = withSpring(0, { damping: 15, stiffness: 100 });
translateX.value = 0;
const timer = setTimeout(() => {
handleHide();
}, duration);
return () => clearTimeout(timer);
}
}, [visible]);
const handleHide = () => {
opacity.value = withTiming(0, { duration: 300 });
translateY.value = withTiming(-100, { duration: 300 }, () => {
runOnJS(hide)();
});
};
const swipeGesture = Gesture.Pan()
.onUpdate((event) => {
translateX.value = event.translationX;
})
.onEnd((event) => {
if (Math.abs(event.translationX) > SWIPE_THRESHOLD) {
translateX.value = withTiming(
event.translationX > 0 ? SCREEN_WIDTH : -SCREEN_WIDTH,
{ duration: 200 },
() => runOnJS(handleHide)(),
);
} else {
translateX.value = withSpring(0);
}
});
const animatedStyle = useAnimatedStyle(() => ({
opacity: opacity.value,
transform: [
{ translateY: translateY.value },
{ translateX: translateX.value },
],
}));
if (!visible) return null;
const variant = TOAST_VARIANTS[type];
return (
<GestureDetector gesture={swipeGesture}>
<Animated.View
style={[
styles.container,
{
top: insets.top + 10,
borderColor: variant.border,
},
animatedStyle,
]}
className="border-2 rounded-2xl shadow-xl bg-background dark:bg-background dark:shadow-none flex-row items-center p-4 pr-10"
>
<View className="mr-4">{variant.icon}</View>
<View className="flex-1">
<Text className="font-bold text-[14px] text-foreground mb-1 leading-tight">
{title}
</Text>
<Text className="text-muted-foreground text-xs font-medium leading-normal">
{message}
</Text>
</View>
</Animated.View>
</GestureDetector>
);
}
const styles = StyleSheet.create({
container: {
position: "absolute",
left: 16,
right: 16,
zIndex: 9999,
},
});