feat: add expo mobile application source code

This commit is contained in:
2026-05-21 16:06:35 +07:00
parent 76d7a5c5c6
commit 0c65a7811b
77 changed files with 20356 additions and 0 deletions
+120
View File
@@ -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 },
});