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>
134 lines
3.5 KiB
TypeScript
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,
|
|
},
|
|
});
|