Files

304 lines
9.8 KiB
TypeScript

import React, { useEffect, useRef } from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
TextInput,
Dimensions,
ActivityIndicator,
Platform,
Animated,
Easing
} from 'react-native';
import * as Haptics from 'expo-haptics';
import { LinearGradient } from 'expo-linear-gradient';
import { MaterialCommunityIcons, Feather } from '@expo/vector-icons';
import { useAppTheme } from '../context/ThemeContext';
const { width } = Dimensions.get('window');
// ── AICard: Premium Clean Card ───────────────────────
export const AICard = ({ children, style, delay = 0, variant = 'white' }: any) => {
const { isDark, colors } = useAppTheme();
const fadeAnim = useRef(new Animated.Value(0)).current;
const slideAnim = useRef(new Animated.Value(30)).current;
useEffect(() => {
Animated.parallel([
Animated.timing(fadeAnim, {
toValue: 1,
duration: 500,
delay,
useNativeDriver: true,
}),
Animated.timing(slideAnim, {
toValue: 0,
duration: 500,
delay,
easing: Easing.out(Easing.back(1.5)),
useNativeDriver: true,
})
]).start();
}, [delay]);
let bg: string;
if (variant === 'white') bg = colors.surface;
else if (variant === 'lime') bg = colors.primary;
else if (variant === 'dark') bg = colors.secondary;
else bg = variant;
return (
<Animated.View
style={[
styles.card,
{
backgroundColor: bg,
borderColor: colors.border,
shadowColor: isDark ? '#000' : '#1A1A1A',
opacity: fadeAnim,
transform: [{ translateY: slideAnim }]
},
style
]}
>
{children}
</Animated.View>
);
};
// ── AIButton: High-End CTA Button ────────────────────
export const AIButton = ({ title, onPress, loading, icon, color, style, textStyle }: any) => {
const { isDark, colors } = useAppTheme();
const handlePress = () => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
if (onPress) onPress();
};
const defaultBg = colors.primary;
const defaultText = colors.secondary;
const btnBg = color || defaultBg;
const isLimeBg = btnBg === colors.primary;
const resolvedTextColor = textStyle?.color || (isLimeBg ? colors.secondary : defaultText);
return (
<TouchableOpacity
onPress={handlePress}
disabled={loading}
activeOpacity={0.82}
style={[
styles.button,
{ backgroundColor: btnBg },
style
]}
>
{loading ? (
<ActivityIndicator color={resolvedTextColor} />
) : (
<View style={styles.buttonContent}>
{icon && (
<MaterialCommunityIcons
name={icon}
size={20}
color={resolvedTextColor}
style={{ marginRight: 8 }}
/>
)}
<Text style={[styles.buttonText, { color: resolvedTextColor }, textStyle]}>{title}</Text>
</View>
)}
</TouchableOpacity>
);
};
// ── AIInput: Themed Input Field ──────────────────────
export const AIInput = ({
label, icon, placeholder, value, onChangeText,
secure, isPassword, style, keyboardType, autoCapitalize = 'none'
}: any) => {
const { colors } = useAppTheme();
const [showPass, setShowPass] = React.useState(false);
const isSecure = secure || isPassword;
return (
<View style={[styles.inputGroup, style]}>
{label && (
<Text style={[styles.inputLabel, { color: colors.textSecondary }]}>{label}</Text>
)}
<View style={[styles.inputField, {
backgroundColor: colors.background,
borderColor: colors.border,
}]}>
{icon && (
<Feather
name={icon}
size={18}
color={colors.textPlaceholder}
style={{ marginRight: 12 }}
/>
)}
<TextInput
placeholder={placeholder}
placeholderTextColor={colors.textPlaceholder}
value={value}
onChangeText={onChangeText}
secureTextEntry={isSecure && !showPass}
style={[styles.textInput, { color: colors.text }]}
autoCapitalize={autoCapitalize}
keyboardType={keyboardType}
/>
{isSecure && (
<TouchableOpacity onPress={() => setShowPass(!showPass)} style={{ padding: 8 }}>
<Feather
name={showPass ? 'eye-off' : 'eye'}
size={18}
color={colors.textPlaceholder}
/>
</TouchableOpacity>
)}
</View>
</View>
);
};
// ── AISectionHeader: Section Title ───────────────────
export const AISectionHeader = ({ title, subtitle, action, onAction, style }: any) => {
const { colors } = useAppTheme();
return (
<View style={[styles.sectionHeader, style]}>
<View style={{ flex: 1 }}>
<Text style={[styles.sectionTitle, { color: colors.text }]}>{title}</Text>
{subtitle && (
<Text style={[styles.sectionSub, { color: colors.textSecondary }]}>{subtitle}</Text>
)}
</View>
{action && (
<TouchableOpacity onPress={onAction}>
<Text style={[styles.sectionAction, { color: colors.primary }]}>{action}</Text>
</TouchableOpacity>
)}
</View>
);
};
// ── AILimeBadge: Pill badge with lime accent ──────────
export const AILimeBadge = ({ label, style }: any) => {
const { colors } = useAppTheme();
return (
<View style={[styles.limeBadge, { backgroundColor: colors.primaryMuted }, style]}>
<Text style={[styles.limeBadgeText, { color: colors.primary }]}>{label}</Text>
</View>
);
};
// ── AISkeleton: Premium Shimmer Loader ────────────────
export const AISkeleton = ({ width, height, radius = 12, style }: any) => {
const { isDark, colors } = useAppTheme();
const shimmerAnim = useRef(new Animated.Value(0)).current;
useEffect(() => {
Animated.loop(
Animated.timing(shimmerAnim, {
toValue: 1,
duration: 1500,
easing: Easing.linear,
useNativeDriver: true,
})
).start();
}, []);
const translateX = shimmerAnim.interpolate({
inputRange: [0, 1],
outputRange: [-300, 300]
});
const bg = colors.surface;
const highlight = colors.surfaceElevated;
return (
<View
style={[
{
width: width || '100%',
height: height || 20,
borderRadius: radius,
backgroundColor: bg,
overflow: 'hidden',
},
style || {},
]}
>
<Animated.View style={[{ width: '100%', height: '100%', transform: [{ translateX }] }]}>
<LinearGradient
colors={[bg, highlight, bg]}
start={{ x: 0, y: 0.5 }}
end={{ x: 1, y: 0.5 }}
style={{ flex: 1 }}
/>
</Animated.View>
</View>
);
};
export const AIPressable = ({ children, onPress, style, containerStyle }: any) => {
const scale = useRef(new Animated.Value(1)).current;
const handlePressIn = () => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
Animated.spring(scale, { toValue: 0.97, damping: 10, stiffness: 300, useNativeDriver: true }).start();
};
const handlePressOut = () => {
Animated.spring(scale, { toValue: 1, damping: 10, stiffness: 300, useNativeDriver: true }).start();
};
return (
<TouchableOpacity
activeOpacity={1}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
onPress={onPress}
style={style}
>
<Animated.View style={[containerStyle || { flex: 1 }, { transform: [{ scale }] }]}>
{children}
</Animated.View>
</TouchableOpacity>
);
};
// ── AISuccess: Animated Checkmark ──────────────────
export const AISuccess = ({ size = 80 }: { size?: number }) => {
const { colors } = useAppTheme();
const scale = useRef(new Animated.Value(0)).current;
useEffect(() => {
Animated.spring(scale, { toValue: 1, damping: 12, stiffness: 200, useNativeDriver: true }).start();
}, []);
return (
<Animated.View style={[styles.successCircle, { width: size, height: size, backgroundColor: colors.primary + '20', transform: [{ scale }] }]}>
<Feather name="check" size={size * 0.6} color={colors.primary} />
</Animated.View>
);
};
const styles = StyleSheet.create({
card: { borderRadius: 24, borderWidth: 1, padding: 20, overflow: 'hidden', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.06, shadowRadius: 16, elevation: 3 },
button: { height: 58, borderRadius: 16, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 24 },
buttonContent: { flexDirection: 'row', alignItems: 'center' },
buttonText: { fontSize: 16, fontFamily: 'Outfit_700Bold' },
inputGroup: { marginBottom: 18 },
inputLabel: { fontSize: 12, fontFamily: 'Outfit_600SemiBold', marginBottom: 8, marginLeft: 2, textTransform: 'uppercase', letterSpacing: 0.5 },
inputField: { flexDirection: 'row', alignItems: 'center', height: 56, borderRadius: 14, borderWidth: 1, paddingHorizontal: 16 },
textInput: { flex: 1, fontSize: 15, fontFamily: 'Outfit_500Medium' },
sectionHeader: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 24, marginBottom: 14 },
sectionTitle: { fontSize: 20, fontFamily: 'Outfit_700Bold' },
sectionSub: { fontSize: 13, fontFamily: 'Outfit_400Regular', marginTop: 2 },
sectionAction: { fontSize: 13, fontFamily: 'Outfit_600SemiBold' },
limeBadge: { alignSelf: 'flex-start', paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
limeBadgeText: { fontSize: 11, fontFamily: 'Outfit_700Bold', textTransform: 'uppercase', letterSpacing: 0.5 },
successCircle: { borderRadius: 100, alignItems: 'center', justifyContent: 'center' },
});