/ components / BottomPopup.tsx
BottomPopup.tsx
  1  import React, { useEffect } from 'react';
  2  import {
  3    View,
  4    Text,
  5    TouchableOpacity,
  6    StyleSheet,
  7    Modal,
  8    Dimensions,
  9    TouchableWithoutFeedback,
 10    useColorScheme,
 11    StatusBar,
 12    Platform,
 13  } from 'react-native';
 14  import { Colors, ColorScheme } from '@/constants/Colors';
 15  import { useTheme } from '@/constants/ThemeContext';
 16  import { Ionicons } from '@expo/vector-icons';
 17  import { useSafeAreaInsets } from 'react-native-safe-area-context';
 18  import Animated, {
 19    useSharedValue,
 20    useAnimatedStyle,
 21    withSpring,
 22    withTiming,
 23    Easing,
 24  } from 'react-native-reanimated';
 25  import { BottomPopupProps } from '@/types';
 26  
 27  const BottomPopup: React.FC<BottomPopupProps> = ({
 28    visible,
 29    title,
 30    onClose,
 31    options,
 32  }) => {
 33    const { theme } = useTheme();
 34    const systemColorScheme = useColorScheme() as ColorScheme;
 35    const colorScheme =
 36      theme === 'system' ? systemColorScheme : (theme as ColorScheme);
 37    const colors = Colors[colorScheme];
 38    const insets = useSafeAreaInsets();
 39  
 40    const styles = getStyles(colors, insets);
 41    const screenHeight = Dimensions.get('window').height;
 42  
 43    const translateY = useSharedValue(screenHeight);
 44    const overlayOpacity = useSharedValue(0);
 45  
 46    useEffect(() => {
 47      if (visible) {
 48        // Show the modal
 49        StatusBar.setBarStyle('light-content');
 50        if (Platform.OS === 'android') {
 51          StatusBar.setBackgroundColor('rgba(0,0,0,0.05)');
 52        }
 53        translateY.value = withSpring(0, {
 54          damping: 25,
 55          stiffness: 200,
 56          mass: 0.5,
 57          overshootClamping: false,
 58          restDisplacementThreshold: 0.1,
 59          restSpeedThreshold: 0.1,
 60        });
 61        overlayOpacity.value = withTiming(1, {
 62          duration: 100,
 63          easing: Easing.out(Easing.ease),
 64        });
 65      } else {
 66        // Hide the modal
 67        StatusBar.setBarStyle('default');
 68        if (Platform.OS === 'android') {
 69          StatusBar.setBackgroundColor('transparent');
 70        }
 71        translateY.value = withTiming(screenHeight, {
 72          duration: 100,
 73          easing: Easing.in(Easing.ease),
 74        });
 75        overlayOpacity.value = withTiming(0, {
 76          duration: 100,
 77          easing: Easing.in(Easing.ease),
 78        });
 79      }
 80    }, [visible]);
 81  
 82    const containerAnimatedStyle = useAnimatedStyle(() => {
 83      return {
 84        transform: [{ translateY: translateY.value }],
 85      };
 86    });
 87  
 88    const overlayAnimatedStyle = useAnimatedStyle(() => {
 89      return {
 90        opacity: overlayOpacity.value,
 91      };
 92    });
 93  
 94    return (
 95      <Modal
 96        transparent={true}
 97        visible={visible}
 98        animationType="none"
 99        onRequestClose={onClose}
100        statusBarTranslucent={true}
101      >
102        <View style={styles.modalContainer}>
103          <TouchableWithoutFeedback onPress={onClose}>
104            <Animated.View style={[styles.modalOverlay, overlayAnimatedStyle]} />
105          </TouchableWithoutFeedback>
106          <Animated.View style={[styles.container, containerAnimatedStyle]}>
107            <View style={styles.handle} />
108            <View style={styles.header}>
109              <Text testID="bottom-popup-title" style={styles.title}>
110                {title}
111              </Text>
112              <TouchableOpacity
113                testID="close-button"
114                onPress={onClose}
115                style={styles.closeButton}
116              >
117                <Ionicons name="close" size={24} color={colors.text} />
118              </TouchableOpacity>
119            </View>
120            <View style={styles.optionsContainer}>
121              {options?.map((option, index) => (
122                <TouchableOpacity
123                  key={index}
124                  style={styles.optionButton}
125                  onPress={() => {
126                    option.onPress();
127                    onClose();
128                  }}
129                >
130                  {option.icon && (
131                    <View style={styles.iconContainer}>
132                      <Ionicons
133                        name={option.icon}
134                        size={24}
135                        color={colors.primary}
136                      />
137                    </View>
138                  )}
139                  <Text style={styles.optionText}>{option.text}</Text>
140                </TouchableOpacity>
141              ))}
142            </View>
143          </Animated.View>
144        </View>
145      </Modal>
146    );
147  };
148  
149  const getStyles = (colors: typeof Colors.light, insets: { bottom: number }) =>
150    StyleSheet.create({
151      modalContainer: {
152        flex: 1,
153        justifyContent: 'flex-end',
154      },
155      modalOverlay: {
156        ...StyleSheet.absoluteFillObject,
157        backgroundColor: 'rgba(0,0,0,0.5)',
158      },
159      container: {
160        backgroundColor: colors.card,
161        borderTopLeftRadius: 24,
162        borderTopRightRadius: 24,
163        paddingTop: 12,
164        paddingHorizontal: 24,
165        paddingBottom: insets.bottom + 24,
166        maxHeight: '80%',
167        shadowColor: '#000',
168        shadowOffset: { width: 0, height: -3 },
169        shadowOpacity: 0.1,
170        shadowRadius: 5,
171        elevation: 5,
172      },
173      handle: {
174        width: 40,
175        height: 4,
176        backgroundColor: colors.border,
177        borderRadius: 2,
178        alignSelf: 'center',
179        marginBottom: 16,
180      },
181      header: {
182        flexDirection: 'row',
183        justifyContent: 'space-between',
184        alignItems: 'center',
185        marginBottom: 24,
186      },
187      title: {
188        fontSize: 22,
189        color: colors.text,
190        fontWeight: 'bold',
191      },
192      closeButton: {
193        padding: 8,
194        borderRadius: 20,
195        backgroundColor: colors.background,
196      },
197      optionsContainer: {
198        marginTop: 8,
199      },
200      optionButton: {
201        flexDirection: 'row',
202        alignItems: 'center',
203        paddingVertical: 16,
204        borderBottomWidth: StyleSheet.hairlineWidth,
205        borderBottomColor: colors.border,
206      },
207      iconContainer: {
208        width: 40,
209        height: 40,
210        borderRadius: 20,
211        backgroundColor: colors.background,
212        justifyContent: 'center',
213        alignItems: 'center',
214        marginRight: 16,
215      },
216      optionText: {
217        fontSize: 18,
218        color: colors.text,
219        flex: 1,
220      },
221    });
222  
223  export default BottomPopup;