140 lines
4.0 KiB
TypeScript
140 lines
4.0 KiB
TypeScript
import React, { createContext, useContext, useState, useCallback, useRef } from 'react';
|
|
import { View, Text, StyleSheet, Animated, Platform, Dimensions } from 'react-native';
|
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
|
import { useAppTheme } from './ThemeContext';
|
|
|
|
interface ToastContextType {
|
|
showToast: (message: string, type?: 'success' | 'error' | 'info') => void;
|
|
}
|
|
|
|
const { width } = Dimensions.get('window');
|
|
const LIME = '#C6F135';
|
|
|
|
const ToastContext = createContext<ToastContextType | undefined>(undefined);
|
|
|
|
export function ToastProvider({ children }: { children: React.ReactNode }) {
|
|
const { colors, isDark } = useAppTheme();
|
|
const [toast, setToast] = useState<{ message: string, type: string } | null>(null);
|
|
|
|
const translateY = useRef(new Animated.Value(-120)).current;
|
|
const opacity = useRef(new Animated.Value(0)).current;
|
|
|
|
const showToast = useCallback((message: string, type: 'success' | 'error' | 'info' = 'info') => {
|
|
setToast({ message, type });
|
|
|
|
translateY.setValue(-120);
|
|
opacity.setValue(0);
|
|
|
|
Animated.sequence([
|
|
Animated.parallel([
|
|
Animated.spring(translateY, { toValue: Platform.OS === 'ios' ? 60 : 40, useNativeDriver: true, friction: 9, tension: 50 }),
|
|
Animated.timing(opacity, { toValue: 1, duration: 400, useNativeDriver: true })
|
|
]),
|
|
Animated.delay(2800),
|
|
Animated.parallel([
|
|
Animated.timing(translateY, { toValue: -120, duration: 300, useNativeDriver: true }),
|
|
Animated.timing(opacity, { toValue: 0, duration: 300, useNativeDriver: true })
|
|
])
|
|
]).start(() => setToast(null));
|
|
}, []);
|
|
|
|
// Theme configuration for the toast
|
|
const getMeta = () => {
|
|
if (!toast) return { icon: 'information', color: LIME };
|
|
switch(toast.type) {
|
|
case 'success': return { icon: 'check-circle', color: LIME };
|
|
case 'error': return { icon: 'alert-circle', color: '#EF4444' };
|
|
default: return { icon: 'information', color: '#3B82F6' };
|
|
}
|
|
};
|
|
|
|
const meta = getMeta();
|
|
const toastBg = isDark ? '#1A1A1A' : '#1A1A1A'; // Solid dark toast for both modes is more premium
|
|
const border = isDark ? '#2A2A2A' : '#333';
|
|
|
|
return (
|
|
<ToastContext.Provider value={{ showToast }}>
|
|
{children}
|
|
{toast && (
|
|
<Animated.View
|
|
style={[
|
|
styles.container,
|
|
{
|
|
opacity,
|
|
transform: [{ translateY }],
|
|
zIndex: 10000,
|
|
}
|
|
]}
|
|
pointerEvents="none"
|
|
>
|
|
<View style={[
|
|
styles.toast,
|
|
{
|
|
backgroundColor: toastBg,
|
|
borderColor: border,
|
|
}
|
|
]}>
|
|
<View style={[styles.iconBox, { backgroundColor: `${meta.color}20` }]}>
|
|
<MaterialCommunityIcons
|
|
name={meta.icon as any}
|
|
size={22}
|
|
color={meta.color}
|
|
/>
|
|
</View>
|
|
<Text style={styles.text}>{toast.message}</Text>
|
|
</View>
|
|
</Animated.View>
|
|
)}
|
|
</ToastContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useToast() {
|
|
const context = useContext(ToastContext);
|
|
if (context === undefined) {
|
|
return {
|
|
showToast: (msg: string) => console.log('Toast (Fallback):', msg)
|
|
};
|
|
}
|
|
return context;
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 20,
|
|
right: 20,
|
|
alignItems: 'center',
|
|
},
|
|
toast: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
paddingVertical: 14,
|
|
paddingHorizontal: 16,
|
|
borderRadius: 20,
|
|
borderWidth: 1,
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 15 },
|
|
shadowOpacity: 0.25,
|
|
shadowRadius: 20,
|
|
elevation: 12,
|
|
width: '100%',
|
|
maxWidth: 450,
|
|
},
|
|
iconBox: {
|
|
width: 42,
|
|
height: 42,
|
|
borderRadius: 12,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
text: {
|
|
marginLeft: 14,
|
|
fontSize: 14,
|
|
fontFamily: 'Outfit_700Bold',
|
|
color: '#FFFFFF',
|
|
flexShrink: 1,
|
|
},
|
|
});
|