146 lines
3.7 KiB
TypeScript
146 lines
3.7 KiB
TypeScript
import React, { useEffect, useRef } from "react";
|
|
import { View, StyleSheet, Animated } from "react-native";
|
|
import { CheckCircle2, AlertCircle, AlertTriangle, Lightbulb, X } from "@/lib/icons";
|
|
import { Text } from "@/components/ui/text";
|
|
import { useToast, ToastType } from "@/lib/toast-store";
|
|
import { useColorScheme } from "nativewind";
|
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
|
|
const VARIANT_CONFIG: Record<
|
|
ToastType,
|
|
{ iconColor: string; borderColor: string; icon: React.ReactNode }
|
|
> = {
|
|
success: {
|
|
iconColor: "#16a34a",
|
|
borderColor: "#16a34a",
|
|
icon: <CheckCircle2 size={18} color="#16a34a" strokeWidth={2.5} />,
|
|
},
|
|
error: {
|
|
iconColor: "#dc2626",
|
|
borderColor: "#dc2626",
|
|
icon: <AlertCircle size={18} color="#dc2626" strokeWidth={2.5} />,
|
|
},
|
|
warning: {
|
|
iconColor: "#d97706",
|
|
borderColor: "#d97706",
|
|
icon: <AlertTriangle size={18} color="#d97706" strokeWidth={2.5} />,
|
|
},
|
|
info: {
|
|
iconColor: "#E46212",
|
|
borderColor: "#E46212",
|
|
icon: <Lightbulb size={18} color="#E46212" strokeWidth={2.5} />,
|
|
},
|
|
};
|
|
|
|
export function ModalToast() {
|
|
const { visible, type, title, message, hide, duration } = useToast();
|
|
const isDark = useColorScheme() === "dark";
|
|
const insets = useSafeAreaInsets();
|
|
|
|
const translateX = useRef(new Animated.Value(-40)).current;
|
|
const opacity = useRef(new Animated.Value(0)).current;
|
|
|
|
useEffect(() => {
|
|
if (visible) {
|
|
translateX.setValue(-40);
|
|
opacity.setValue(0);
|
|
|
|
Animated.parallel([
|
|
Animated.spring(translateX, {
|
|
toValue: 0,
|
|
useNativeDriver: true,
|
|
speed: 20,
|
|
bounciness: 6,
|
|
}),
|
|
Animated.timing(opacity, {
|
|
toValue: 1,
|
|
duration: 180,
|
|
useNativeDriver: true,
|
|
}),
|
|
]).start();
|
|
|
|
const timer = setTimeout(hide, duration);
|
|
return () => clearTimeout(timer);
|
|
}
|
|
}, [visible]);
|
|
|
|
if (!visible) return null;
|
|
|
|
const config = VARIANT_CONFIG[type];
|
|
|
|
return (
|
|
<View
|
|
pointerEvents="box-none"
|
|
style={[StyleSheet.absoluteFill, styles.absoluteOverlay]}
|
|
>
|
|
<Animated.View
|
|
style={[
|
|
styles.toast,
|
|
{
|
|
top: insets.top + 12,
|
|
backgroundColor: isDark ? "#1C1C1C" : "#ffffff",
|
|
borderColor: config.borderColor,
|
|
borderWidth: 1,
|
|
transform: [{ translateX }],
|
|
opacity,
|
|
},
|
|
]}
|
|
>
|
|
<View
|
|
style={[
|
|
styles.iconContainer,
|
|
{ backgroundColor: isDark ? "#2a2a2a" : "#f5f5f5" },
|
|
]}
|
|
>
|
|
{config.icon}
|
|
</View>
|
|
|
|
<View style={styles.textContainer}>
|
|
<Text className="text-foreground text-[14px] font-sans-black leading-[18px]">
|
|
{title}
|
|
</Text>
|
|
{message ? (
|
|
<Text className="text-muted-foreground text-[12px] font-sans-medium leading-[16px] mt-1">
|
|
{message}
|
|
</Text>
|
|
) : null}
|
|
</View>
|
|
|
|
<View className="h-6 w-6 rounded-full items-center justify-center">
|
|
<X size={14} color="#9ca3af" strokeWidth={2.5} />
|
|
</View>
|
|
</Animated.View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
absoluteOverlay: {
|
|
zIndex: 9999,
|
|
elevation: 50,
|
|
},
|
|
toast: {
|
|
marginHorizontal: 16,
|
|
borderRadius: 12,
|
|
paddingHorizontal: 16,
|
|
paddingVertical: 12,
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
shadowColor: "#000",
|
|
shadowOpacity: 0.18,
|
|
shadowRadius: 8,
|
|
shadowOffset: { width: 0, height: 4 },
|
|
},
|
|
iconContainer: {
|
|
width: 32,
|
|
height: 32,
|
|
borderRadius: 16,
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
},
|
|
textContainer: {
|
|
flex: 1,
|
|
marginLeft: 12,
|
|
},
|
|
});
|