Files
Abin ToppoandClaude Fable 5 1d7646d938
CI / quality (push) Waiting to run
feat: architecture refactor — core layer, spreadsheet engine, envelope bidding, tests
- Add src/core/ (config, errors, events, http, observability, storage, query)
- Add src/Spreadsheet/ engine with native POI backend and Excel bridge
- Add Envelope C spreadsheet flow and tender bidding envelope modules
- Consolidate components into src/components/, remove legacy Component/ and Hooks/
- Add registration state/validation, auth session guard, profile edit modules
- Add unit tests across axios, redux, bidding, registration, encryption, core
- Add CI workflow, docs (architecture, audit, DSC API spec), keystore example
- Remove tracked keystore binary; ignore scripts.zip

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 10:20:22 +05:30

267 lines
9.1 KiB
TypeScript

import React, { useEffect } from 'react';
import {
View,
TouchableOpacity,
StyleSheet,
Dimensions,
Platform,
} from 'react-native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import * as LucideIcons from 'lucide-react-native';
import Animated, {
useAnimatedStyle,
useSharedValue,
withSpring,
withTiming,
interpolate,
Extrapolation
} from 'react-native-reanimated';
import Screens from '../Screens';
import NavigationString from '../Constant/NavigationString';
import { useTheme } from '../Hooks/useTheme';
import { typography } from '../theme/typography';
// import EditableExcel from '../Screens/Tender/Component/EnvelopeCExcelPreview';
const Tab = createBottomTabNavigator();
const TabButton = ({ route, isFocused, onPress }: any) => {
const { colors } = useTheme();
const icons: Record<string, { outline: keyof typeof LucideIcons; filled: keyof typeof LucideIcons }> = {
Home: { outline: "House", filled: "House" },
Auction: { outline: "Gavel", filled: "Gavel" },
Tender: { outline: "FileText", filled: "FileText" },
Profile: { outline: "User", filled: "User" },
};
// Lucide icons don't always have 'filled' variants in the set, so we might use the same icon
// or rely on color/strokeWidth to indicate state.
const iconKey = icons[route.name]?.outline || "HelpCircle"; // Fallback
const IconName = LucideIcons[iconKey] as React.ElementType;
const scale = useSharedValue(0);
const translateY = useSharedValue(0);
useEffect(() => {
if (isFocused) {
scale.value = withSpring(1, { damping: 12, stiffness: 100 });
translateY.value = withSpring(-4, { damping: 12, stiffness: 100 });
} else {
scale.value = withTiming(0, { duration: 200 });
translateY.value = withSpring(0, { damping: 12, stiffness: 100 });
}
// Intentionally runs only when isFocused changes; other deps are stable or recreated each render.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isFocused]);
useAnimatedStyle(() => {
return {
transform: [{ scale: scale.value }],
opacity: interpolate(scale.value, [0, 1], [0, 1], Extrapolation.CLAMP),
};
});
const animatedIconStyle = useAnimatedStyle(() => {
return {
transform: [{ scale: interpolate(scale.value, [0, 1], [1, 0.9]) }], // Slight shrink on press
};
});
const animatedTextStyle = useAnimatedStyle(() => {
const opacity = interpolate(scale.value, [0, 1], [0, 1]);
const translateX = interpolate(scale.value, [0, 1], [20, 0]); // Slide in from right
return {
opacity,
transform: [{ translateX }],
display: scale.value === 0 ? 'none' : 'flex', // Hide when inactive
marginLeft: 4, // Reduced space
};
});
return (
<TouchableOpacity
onPress={onPress}
style={styles.tabButton}
activeOpacity={0.8}
accessibilityRole="tab"
accessibilityState={{ selected: isFocused }}
accessibilityLabel={`${route.name} tab`}
>
<View style={styles.tabContent}>
<Animated.View style={animatedIconStyle}>
{IconName && (
<IconName
size={24}
color={isFocused ? colors.primary : colors.text}
strokeWidth={isFocused ? 2.5 : 2}
/>
)}
</Animated.View>
{isFocused && (
<Animated.Text
style={[
styles.label,
animatedTextStyle,
{
color: colors.primary,
fontFamily: typography.fonts.medium,
fontWeight: undefined, // Remove fontWeight as we use fontFamily
},
]}
numberOfLines={1}
>
{route.name === 'BrandDay' ? 'Coupons' : route.name}
</Animated.Text>
)}
</View>
</TouchableOpacity>
);
};
const CustomTabBar = ({ state, navigation }: any) => {
const { colors } = useTheme();
const { width: windowWidth } = Dimensions.get('window');
const TAB_BAR_WIDTH = windowWidth - 40; // 20 margin each side
const TAB_PADDING_H = 10; // 5 padding left + 5 padding right
const TAB_WIDTH = (TAB_BAR_WIDTH - TAB_PADDING_H) / state.routes.length;
const translateX = useSharedValue(0);
useEffect(() => {
translateX.value = withSpring(state.index * TAB_WIDTH, {
damping: 15,
stiffness: 150,
mass: 0.5,
});
// Intentionally runs only when the selected tab index changes; other deps are stable or recreated each render.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [state.index]);
const animatedIndicatorStyle = useAnimatedStyle(() => {
return {
transform: [{ translateX: translateX.value }],
};
});
return (
<View style={[
styles.tabBar,
{
backgroundColor: colors.background, // Use transparent to allow shadow to show
shadowColor: colors.shadow, // Dynamic shadow color
shadowOffset: { width: 0, height: 10 },
shadowOpacity: 0.2, // Consistent opacity
shadowRadius: 10,
borderColor: colors.border, // Subtle border for definition
borderWidth: 3,
elevation: 20,
}
]}>
<Animated.View
style={[
styles.slidingIndicator,
{
width: TAB_WIDTH,
backgroundColor: colors.primary,
opacity: 0.3,
elevation: 10, // Subtle elevation for the indicator
},
animatedIndicatorStyle,
]}
/>
{state.routes.map((route: any, index: number) => {
const isFocused = state.index === index;
return (
<TabButton
key={route.key}
route={route}
isFocused={isFocused}
onPress={() => navigation.navigate(route.name)}
/>
);
})}
</View>
);
};
export default function BottomTab() {
const { colors } = useTheme();
return (
<View style={[styles.container, { backgroundColor: colors.background }]}>
<Tab.Navigator
tabBar={(props) => <CustomTabBar {...props} />}
screenOptions={{
headerShown: false,
tabBarHideOnKeyboard: true,
// Smooth cross-fade when switching tabs instead of an instant cut.
animation: 'shift',
}}
>
<Tab.Screen name="Home" component={Screens.Home} />
<Tab.Screen name={NavigationString.Auction} component={Screens.Auction} />
<Tab.Screen name={NavigationString.Tender} component={Screens.Tender} />
{/* <Tab.Screen name={'EditableExcel'} component={EditableExcel} /> */}
<Tab.Screen name={NavigationString.Profile} component={Screens.ProfileTab} />
</Tab.Navigator>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
tabBar: {
position: 'absolute',
bottom: 20,
left: 20,
right: 20,
flexDirection: 'row',
height: 70,
borderRadius: 35,
elevation: 20, // stronger Android elevation
shadowColor: '#000', // iOS shadow color
shadowOffset: { width: 0, height: 10 },
shadowOpacity: Platform.OS === 'ios' ? 0.25 : 0.2,
shadowRadius: 12,
overflow: 'visible', // allow shadow to render outside rounded corners
zIndex: 9999,
paddingBottom: 0,
justifyContent: 'space-around',
alignItems: 'center',
paddingHorizontal: 5,
},
tabButton: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
height: '100%',
},
tabContent: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 4, // Drastically reduced from 12
paddingVertical: 4,
borderRadius: 20,
},
slidingIndicator: {
position: 'absolute',
top: 12,
left: 5,
height: 40,
borderRadius: 30,
zIndex: 0,
},
label: {
fontSize: 11, // Slightly smaller text
textAlign: 'left',
textTransform: 'capitalize',
letterSpacing: 0.2, // Tighter spacing
},
});