192 lines
6.3 KiB
TypeScript
192 lines
6.3 KiB
TypeScript
import React, { type ReactNode, useEffect, useRef, useMemo } from "react";
|
|
import {
|
|
Modal as RNModal,
|
|
View,
|
|
ScrollView,
|
|
TouchableOpacity,
|
|
Text,
|
|
StyleSheet,
|
|
Animated,
|
|
} from "react-native";
|
|
import { Ionicons } from "@expo/vector-icons";
|
|
import { spacing, borderRadius, fontSize } from "../../theme";
|
|
import { useTheme } from "../../context/ThemeContext";
|
|
|
|
interface ModalProps {
|
|
visible: boolean;
|
|
onClose: () => void;
|
|
title?: string;
|
|
children: ReactNode;
|
|
icon?: keyof typeof Ionicons.glyphMap;
|
|
iconColor?: string;
|
|
}
|
|
|
|
export default function Modal({
|
|
visible,
|
|
onClose,
|
|
title,
|
|
children,
|
|
icon,
|
|
iconColor,
|
|
}: ModalProps) {
|
|
const { colors } = useTheme();
|
|
const scale = useRef(new Animated.Value(0.85)).current;
|
|
const opacity = useRef(new Animated.Value(0)).current;
|
|
|
|
useEffect(() => {
|
|
if (visible) {
|
|
Animated.parallel([
|
|
Animated.spring(scale, {
|
|
toValue: 1,
|
|
useNativeDriver: true,
|
|
tension: 65,
|
|
friction: 8,
|
|
}),
|
|
Animated.timing(opacity, {
|
|
toValue: 1,
|
|
duration: 200,
|
|
useNativeDriver: true,
|
|
}),
|
|
]).start();
|
|
} else {
|
|
scale.setValue(0.85);
|
|
opacity.setValue(0);
|
|
}
|
|
}, [visible]);
|
|
|
|
const styles = useMemo(
|
|
() =>
|
|
StyleSheet.create({
|
|
overlay: {
|
|
flex: 1,
|
|
backgroundColor: "rgba(0,0,0,0.85)",
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
padding: spacing.xl,
|
|
},
|
|
content: {
|
|
backgroundColor: colors.bgSecondary,
|
|
borderRadius: 20,
|
|
width: "100%",
|
|
maxHeight: "80%",
|
|
borderWidth: 1,
|
|
borderColor: "rgba(255,255,255,0.08)",
|
|
overflow: "hidden",
|
|
shadowColor: colors.accent,
|
|
shadowOffset: { width: 0, height: 0 },
|
|
shadowOpacity: 0.15,
|
|
shadowRadius: 30,
|
|
elevation: 20,
|
|
},
|
|
accentBar: {
|
|
height: 3,
|
|
backgroundColor: colors.accent,
|
|
width: "100%",
|
|
},
|
|
header: {
|
|
flexDirection: "row",
|
|
justifyContent: "space-between",
|
|
alignItems: "center",
|
|
paddingHorizontal: spacing.xl,
|
|
paddingTop: spacing.l,
|
|
paddingBottom: spacing.m,
|
|
},
|
|
titleRow: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: 10,
|
|
flex: 1,
|
|
},
|
|
iconCircle: {
|
|
width: 36,
|
|
height: 36,
|
|
borderRadius: 18,
|
|
backgroundColor: colors.accent + "20",
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
},
|
|
title: {
|
|
color: colors.textWhite,
|
|
fontSize: fontSize.lg,
|
|
fontWeight: "700",
|
|
flex: 1,
|
|
},
|
|
closeBtn: {
|
|
width: 32,
|
|
height: 32,
|
|
borderRadius: 16,
|
|
backgroundColor: "rgba(255,255,255,0.06)",
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
},
|
|
body: {
|
|
paddingHorizontal: spacing.xl,
|
|
},
|
|
}),
|
|
[colors],
|
|
);
|
|
|
|
return (
|
|
<RNModal
|
|
visible={visible}
|
|
transparent
|
|
animationType="fade"
|
|
onRequestClose={onClose}
|
|
>
|
|
<View style={styles.overlay}>
|
|
<Animated.View
|
|
style={[
|
|
styles.content,
|
|
{ transform: [{ scale }], opacity },
|
|
]}
|
|
>
|
|
<View style={styles.accentBar} />
|
|
<View style={styles.header}>
|
|
<View style={styles.titleRow}>
|
|
{icon && (
|
|
<View
|
|
style={[
|
|
styles.iconCircle,
|
|
iconColor
|
|
? {
|
|
backgroundColor:
|
|
iconColor + "20",
|
|
}
|
|
: null,
|
|
]}
|
|
>
|
|
<Ionicons
|
|
name={icon}
|
|
size={20}
|
|
color={iconColor || colors.accent}
|
|
/>
|
|
</View>
|
|
)}
|
|
{title && <Text style={styles.title}>{title}</Text>}
|
|
</View>
|
|
<TouchableOpacity
|
|
onPress={onClose}
|
|
style={styles.closeBtn}
|
|
activeOpacity={0.7}
|
|
>
|
|
<Ionicons
|
|
name="close"
|
|
size={20}
|
|
color={colors.textMuted}
|
|
/>
|
|
</TouchableOpacity>
|
|
</View>
|
|
<ScrollView
|
|
style={styles.body}
|
|
contentContainerStyle={{ paddingBottom: spacing.xl }}
|
|
showsVerticalScrollIndicator={false}
|
|
keyboardShouldPersistTaps="handled"
|
|
>
|
|
{children}
|
|
</ScrollView>
|
|
</Animated.View>
|
|
</View>
|
|
</RNModal>
|
|
);
|
|
}
|