feat: add expo mobile application source code
This commit is contained in:
@@ -0,0 +1,303 @@
|
||||
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' },
|
||||
});
|
||||
Reference in New Issue
Block a user