learning_ai_invt_trdg/mobile/components/CustomTabBar.tsx
root e2008f70b9 fix: web + mobile pre-beta audit — real APIs, socket routing, empty states
Web:
- runtime.ts: use import.meta.env (process.env is undefined in Vite browser bundle)
- tradingApiUrl local fallback: drop /api suffix (API libs already append /api/*)
- useWebSocket: deriveSocketParams() — correctly splits origin + socket path for
  Caddy handle_path /invttrdg/* proxy (io(origin, {path}), not io(url-with-path))
- App.tsx: pass socket prop to AdminTab; pass connected prop to SignalsTab
- AdminTab: remove duplicate useWebSocket; accept socket as prop
- SignalsTab: connection-aware empty state message
- backtest/flags: default to disabled when VITE_BACKTEST_ENABLED unset
- EntryForm: NaN guard before live trade execution
- MarketplaceTab: null-safety on symbols.rules access
- Tests: pass socket prop to AdminTab; update empty state assertion

Mobile:
- TradingDataProvider: same deriveSocketParams fix — EXPO_PUBLIC_SOCKET_PATH
  overrides auto-derived path from tradingApiUrl
- strategies: replace mock data with real GET /api/profiles + PATCH active toggle
- chat: wire to real POST /api/chat; remove hardcoded mock reply
- marketplace: fetch GET /api/marketplace-presets; USE STRATEGY calls POST /api/profiles
- settings: sign-out confirmation dialog; execution mode read-only hint;
  version from expo-constants instead of hardcoded v2.3
- positions/history: empty state UI when no data
- CustomTabBar: always show tab labels (not only when focused)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 04:50:51 +00:00

134 lines
3.5 KiB
TypeScript

import React from 'react';
import { View, Text, Pressable, StyleSheet, Platform } from 'react-native';
import { BottomTabBarProps } from '@react-navigation/bottom-tabs';
import { Activity, Layers, Clock, Cpu, FileSliders as Sliders } from 'lucide-react-native';
import Animated, {
useAnimatedStyle,
useSharedValue,
withSpring,
} from 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Colors, Fonts, FontSize } from '@/constants/theme';
import { triggerHaptic } from '@/utils/haptics';
const TAB_ICONS = [Activity, Layers, Clock, Cpu, Sliders];
const TAB_LABELS = ['Dashboard', 'Positions', 'History', 'Strategies', 'Settings'];
function TabItem({
index,
isFocused,
onPress,
}: {
index: number;
isFocused: boolean;
onPress: () => void;
}) {
const scale = useSharedValue(1);
const Icon = TAB_ICONS[index];
const label = TAB_LABELS[index];
const animStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
React.useEffect(() => {
scale.value = withSpring(isFocused ? 1.1 : 1, { damping: 15, stiffness: 150 });
}, [isFocused]);
return (
<Pressable
style={styles.tabItem}
onPress={() => {
triggerHaptic('light');
onPress();
}}
>
<Animated.View style={[styles.iconContainer, animStyle]}>
<Icon
size={22}
color={isFocused ? Colors.accent.green : Colors.text.secondary}
strokeWidth={isFocused ? 2.5 : 1.8}
/>
{isFocused && <View style={styles.glowDot} />}
</Animated.View>
<Text style={[styles.activeLabel, !isFocused && styles.inactiveLabel]}>{label}</Text>
</Pressable>
);
}
export default function CustomTabBar({ state, navigation }: BottomTabBarProps) {
const insets = useSafeAreaInsets();
return (
<View style={[styles.container, { paddingBottom: Math.max(insets.bottom, 12) }]}>
{state.routes.map((route, index) => {
const isFocused = state.index === index;
const onPress = () => {
const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true,
});
if (!isFocused && !event.defaultPrevented) {
navigation.navigate(route.name);
}
};
return (
<TabItem
key={route.key}
index={index}
isFocused={isFocused}
onPress={onPress}
/>
);
})}
</View>
);
}
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
backgroundColor: Colors.background.primary,
borderTopWidth: 1,
borderTopColor: Colors.border.default,
paddingTop: 10,
},
tabItem: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
gap: 4,
},
iconContainer: {
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
},
glowDot: {
position: 'absolute',
bottom: -8,
width: 4,
height: 4,
borderRadius: 2,
backgroundColor: Colors.accent.green,
shadowColor: Colors.accent.green,
shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0.8,
shadowRadius: 4,
elevation: 4,
},
activeLabel: {
fontFamily: Fonts.inter.extraBold,
fontSize: 9,
color: Colors.accent.green,
textTransform: 'uppercase',
letterSpacing: 1,
marginTop: 6,
},
inactiveLabel: {
color: Colors.text.secondary,
fontFamily: Fonts.inter.medium,
},
});