feat: add expo mobile application source code
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { View, Text, StyleSheet, Modal, TouchableOpacity, ScrollView, Pressable, Platform, Animated, Easing } from 'react-native';
|
||||
import { Feather } from '@expo/vector-icons';
|
||||
import { useAppTheme } from '../context/ThemeContext';
|
||||
|
||||
interface PopupProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
showCloseBtn?: boolean;
|
||||
type?: 'center' | 'bottom';
|
||||
}
|
||||
|
||||
export const Popup: React.FC<PopupProps> = ({
|
||||
visible,
|
||||
onClose,
|
||||
title,
|
||||
children,
|
||||
showCloseBtn = true,
|
||||
type = 'center'
|
||||
}) => {
|
||||
const { colors, isDark } = useAppTheme();
|
||||
const [shouldRender, setShouldRender] = useState(visible);
|
||||
|
||||
const opacity = useRef(new Animated.Value(0)).current;
|
||||
const scale = useRef(new Animated.Value(type === 'center' ? 0.9 : 1)).current;
|
||||
const translateY = useRef(new Animated.Value(type === 'bottom' ? 600 : 0)).current;
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
setShouldRender(true);
|
||||
Animated.parallel([
|
||||
Animated.timing(opacity, { toValue: 1, duration: 300, useNativeDriver: true }),
|
||||
type === 'center'
|
||||
? Animated.spring(scale, { toValue: 1, damping: 15, stiffness: 100, useNativeDriver: true })
|
||||
: Animated.spring(translateY, { toValue: 0, damping: 20, stiffness: 90, useNativeDriver: true })
|
||||
]).start();
|
||||
} else {
|
||||
Animated.parallel([
|
||||
Animated.timing(opacity, { toValue: 0, duration: 250, useNativeDriver: true }),
|
||||
type === 'center'
|
||||
? Animated.timing(scale, { toValue: 0.95, duration: 250, useNativeDriver: true })
|
||||
: Animated.timing(translateY, { toValue: 600, duration: 250, useNativeDriver: true })
|
||||
]).start(() => {
|
||||
setShouldRender(false);
|
||||
});
|
||||
}
|
||||
}, [visible, type]);
|
||||
|
||||
if (!shouldRender) return null;
|
||||
|
||||
const cardBg = isDark ? '#1A1A1A' : '#FFFFFF';
|
||||
const border = isDark ? '#2A2A2A' : '#EEEEEE';
|
||||
|
||||
return (
|
||||
<Modal visible={shouldRender} transparent statusBarTranslucent animationType="none" onRequestClose={onClose}>
|
||||
<View style={[
|
||||
styles.overlay,
|
||||
type === 'bottom' && { justifyContent: 'flex-end', padding: 0 }
|
||||
]}>
|
||||
<Animated.View style={[StyleSheet.absoluteFill, { backgroundColor: 'rgba(0,0,0,0.7)', opacity }]} />
|
||||
<Pressable style={StyleSheet.absoluteFill} onPress={onClose} />
|
||||
|
||||
<Animated.View style={[
|
||||
styles.content,
|
||||
{
|
||||
backgroundColor: cardBg,
|
||||
borderColor: border,
|
||||
borderWidth: 1,
|
||||
opacity,
|
||||
transform: [
|
||||
{ scale: type === 'center' ? scale : 1 },
|
||||
{ translateY: type === 'bottom' ? translateY : 0 }
|
||||
]
|
||||
},
|
||||
type === 'bottom' && {
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
borderTopLeftRadius: 36,
|
||||
borderTopRightRadius: 36,
|
||||
maxWidth: '100%',
|
||||
maxHeight: '92%',
|
||||
}
|
||||
]}>
|
||||
{type === 'bottom' && (
|
||||
<View style={[styles.handle, { backgroundColor: isDark ? '#333' : '#E0E0E0' }]} />
|
||||
)}
|
||||
|
||||
<View style={styles.header}>
|
||||
<Text style={[styles.title, { color: colors.text }]}>{title}</Text>
|
||||
{showCloseBtn && (
|
||||
<TouchableOpacity onPress={onClose} style={[styles.closeBtn, { backgroundColor: isDark ? '#222' : '#F5F5F5' }]}>
|
||||
<Feather name="x" size={18} color={isDark ? '#FFF' : '#1A1A1A'} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
{children}
|
||||
</ScrollView>
|
||||
</Animated.View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
overlay: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 },
|
||||
content: { width: '100%', maxWidth: 420, maxHeight: '85%', borderRadius: 28, overflow: 'hidden', elevation: 20 },
|
||||
handle: { width: 40, height: 4, borderRadius: 2, alignSelf: 'center', marginTop: 12 },
|
||||
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingTop: 24, paddingBottom: 16 },
|
||||
title: { fontSize: 22, fontFamily: 'Outfit_800ExtraBold', letterSpacing: -0.5 },
|
||||
closeBtn: { width: 34, height: 34, borderRadius: 12, alignItems: 'center', justifyContent: 'center' },
|
||||
scrollContent: { paddingHorizontal: 24, paddingBottom: Platform.OS === 'ios' ? 40 : 24 },
|
||||
});
|
||||
Reference in New Issue
Block a user