CI / quality (push) Waiting to run
- 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>
348 lines
10 KiB
TypeScript
348 lines
10 KiB
TypeScript
import React, { useState, useRef, useMemo } from 'react';
|
|
import {
|
|
View,
|
|
StyleSheet,
|
|
TouchableOpacity,
|
|
Animated,
|
|
Dimensions,
|
|
} from 'react-native';
|
|
import { InsideHeader } from '../../components';
|
|
import {
|
|
useFetchLiveTender,
|
|
useFetchPurchaseTender,
|
|
} from '../../Services/ApiCall/Hooks';
|
|
import { ShoppingCart, Lock, Radio, Package } from 'lucide-react-native';
|
|
import { useNavigation } from '@react-navigation/native';
|
|
import NavigationString from '../../Constant/NavigationString';
|
|
import { useSelector } from 'react-redux';
|
|
import LiveTender from './LiveTender';
|
|
import PurchaseTenderAllList from './PurchaseTenderAllList';
|
|
import { useTheme } from '../../Hooks/useTheme';
|
|
import AppText from '../../components/AppText';
|
|
|
|
const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
|
const TAB_HEIGHT = 48;
|
|
const HORIZONTAL_PADDING = 32;
|
|
const SWITCH_INSETS = 3;
|
|
|
|
const Tender = () => {
|
|
const { colors, spacing, shadows } = useTheme();
|
|
const Navigation = useNavigation() as any;
|
|
const { data: liveTenderData }: any = useFetchLiveTender();
|
|
const { data: purchaseTenderData }: any = useFetchPurchaseTender();
|
|
const SelectedPurchaseList = useSelector((state: any) => state?.selectPurchaseTender);
|
|
const reduxToken = useSelector((state: any) => state.token?.token);
|
|
|
|
const liveCount = liveTenderData?.pages?.[0]?.totalCount || 0;
|
|
const purchaseCount = purchaseTenderData?.pages?.[0]?.totalCount || 0;
|
|
|
|
const TAB_WIDTH = useMemo(() => {
|
|
const availableWidth = SCREEN_WIDTH - HORIZONTAL_PADDING - SWITCH_INSETS * 2;
|
|
return availableWidth / 2;
|
|
}, []);
|
|
|
|
const [selectedTab, setSelectedTab] = useState<'live' | 'purchase'>('purchase');
|
|
const translateX = useRef(new Animated.Value(TAB_WIDTH)).current;
|
|
|
|
const onTabPress = (tab: 'live' | 'purchase', index: number) => {
|
|
setSelectedTab(tab);
|
|
Animated.spring(translateX, {
|
|
toValue: index * TAB_WIDTH,
|
|
useNativeDriver: true,
|
|
friction: 8,
|
|
}).start();
|
|
};
|
|
|
|
const isAuctionUser = reduxToken?.userType?.toLowerCase() === 'auction';
|
|
const cartCount = SelectedPurchaseList.length;
|
|
|
|
const styles = makeStyles(colors, spacing, shadows, TAB_WIDTH, TAB_HEIGHT);
|
|
|
|
if (isAuctionUser) {
|
|
return (
|
|
<View style={styles.container}>
|
|
<InsideHeader title="Tender" showArrow />
|
|
<View style={styles.restrictedContainer}>
|
|
<View style={styles.restrictedCard}>
|
|
{/* Double-ring Lock Icon */}
|
|
<View style={styles.lockRing}>
|
|
<View style={styles.lockInner}>
|
|
<Lock size={40} color={colors.primary} strokeWidth={1.8} />
|
|
</View>
|
|
</View>
|
|
|
|
<AppText variant="h2" centered color={colors.text} style={styles.restrictedTitle as any}>
|
|
Access Restricted
|
|
</AppText>
|
|
|
|
<AppText variant="body" centered color={colors.textSecondary} style={styles.restrictedMessage as any}>
|
|
You're logged in as an{' '}
|
|
<AppText variant="body" color={colors.primary} style={{ fontWeight: '700' } as any}>
|
|
Auction user
|
|
</AppText>
|
|
{'. '}Tender features are not available for your account type.
|
|
</AppText>
|
|
|
|
<View style={styles.hintBox}>
|
|
<AppText variant="bodySmall" centered color={colors.textSecondary}>
|
|
Switch to a Tender account to access live & purchase tender features.
|
|
</AppText>
|
|
</View>
|
|
|
|
<TouchableOpacity
|
|
style={styles.loginButton}
|
|
activeOpacity={0.85}
|
|
onPress={() => Navigation.navigate(NavigationString.Login)}
|
|
>
|
|
<AppText variant="button" color={colors.buttonText} style={styles.loginBtnText as any}>
|
|
Switch to Tender Login
|
|
</AppText>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<InsideHeader title="Tender" showArrow />
|
|
|
|
{/* Tab Row */}
|
|
<View style={styles.topRow}>
|
|
{/* Segmented Control */}
|
|
<View
|
|
style={[
|
|
styles.switchWrapper,
|
|
{ width: TAB_WIDTH * 2 + SWITCH_INSETS * 2 },
|
|
]}
|
|
>
|
|
{/* Sliding pill */}
|
|
<Animated.View
|
|
pointerEvents="none"
|
|
style={[
|
|
styles.activeTab,
|
|
{ width: TAB_WIDTH, transform: [{ translateX }] },
|
|
]}
|
|
/>
|
|
|
|
{/* Live Tab */}
|
|
<TouchableOpacity
|
|
style={[styles.tab, { width: TAB_WIDTH }]}
|
|
activeOpacity={0.8}
|
|
onPress={() => onTabPress('live', 0)}
|
|
>
|
|
<Radio
|
|
size={13}
|
|
color={selectedTab === 'live' ? colors.primary : colors.textSecondary}
|
|
strokeWidth={2}
|
|
/>
|
|
<AppText
|
|
variant="caption"
|
|
color={selectedTab === 'live' ? colors.primary : colors.textSecondary}
|
|
style={[
|
|
styles.tabText,
|
|
selectedTab === 'live' && (styles.activeTabText as any),
|
|
]}
|
|
>
|
|
Live {liveCount ? `(${liveCount})` : ''}
|
|
</AppText>
|
|
</TouchableOpacity>
|
|
|
|
{/* Purchase Tab */}
|
|
<TouchableOpacity
|
|
style={[styles.tab, { width: TAB_WIDTH }]}
|
|
activeOpacity={0.8}
|
|
onPress={() => onTabPress('purchase', 1)}
|
|
>
|
|
<Package
|
|
size={13}
|
|
color={selectedTab === 'purchase' ? colors.primary : colors.textSecondary}
|
|
strokeWidth={2}
|
|
/>
|
|
<AppText
|
|
variant="caption"
|
|
color={selectedTab === 'purchase' ? colors.primary : colors.textSecondary}
|
|
style={[
|
|
styles.tabText,
|
|
selectedTab === 'purchase' && (styles.activeTabText as any),
|
|
]}
|
|
>
|
|
Purchased {purchaseCount ? `(${purchaseCount})` : ''}
|
|
</AppText>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Content */}
|
|
{selectedTab === 'live' ? <LiveTender /> : <PurchaseTenderAllList />}
|
|
|
|
{/* Cart Floating Button */}
|
|
<TouchableOpacity
|
|
style={styles.cartFloatingButton}
|
|
activeOpacity={0.9}
|
|
onPress={() => Navigation.navigate(NavigationString.PurchaseTenderList)}
|
|
>
|
|
<ShoppingCart size={24} color={colors.buttonText} strokeWidth={2} />
|
|
{cartCount > 0 && (
|
|
<View style={styles.cartBadge}>
|
|
<AppText variant="caption" color={colors.primary} style={styles.cartBadgeText as any}>
|
|
{cartCount}
|
|
</AppText>
|
|
</View>
|
|
)}
|
|
</TouchableOpacity>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
export default Tender;
|
|
|
|
/* ─── Styles factory ─── */
|
|
const makeStyles = (colors: any, spacing: any, shadows: any, TAB_WIDTH: number, TAB_HEIGHT: number) =>
|
|
StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: colors.background,
|
|
},
|
|
|
|
/* ── Tab Row ── */
|
|
topRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
paddingHorizontal: spacing.m,
|
|
paddingVertical: spacing.s + 2,
|
|
},
|
|
switchWrapper: {
|
|
flexDirection: 'row',
|
|
backgroundColor: colors.surface,
|
|
borderRadius: 16,
|
|
padding: SWITCH_INSETS,
|
|
height: TAB_HEIGHT,
|
|
...shadows.light,
|
|
},
|
|
tab: {
|
|
height: TAB_HEIGHT - SWITCH_INSETS * 2,
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
gap: 5,
|
|
zIndex: 2,
|
|
},
|
|
tabText: {
|
|
fontWeight: '600',
|
|
letterSpacing: 0.3,
|
|
} as any,
|
|
activeTabText: {
|
|
fontWeight: '700',
|
|
} as any,
|
|
activeTab: {
|
|
position: 'absolute',
|
|
top: SWITCH_INSETS,
|
|
left: SWITCH_INSETS,
|
|
height: TAB_HEIGHT - SWITCH_INSETS * 2,
|
|
backgroundColor: colors.background,
|
|
borderRadius: 13,
|
|
...shadows.medium,
|
|
},
|
|
|
|
/* ── Cart Floating Button ── */
|
|
cartFloatingButton: {
|
|
position: 'absolute',
|
|
bottom: spacing.xl + 80,
|
|
right: spacing.l,
|
|
width: 56,
|
|
height: 56,
|
|
borderRadius: 28,
|
|
backgroundColor: colors.primary,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
...shadows.strong,
|
|
zIndex: 999,
|
|
},
|
|
cartBadge: {
|
|
position: 'absolute',
|
|
top: 0,
|
|
right: 0,
|
|
minWidth: 20,
|
|
height: 20,
|
|
borderRadius: 10,
|
|
backgroundColor: colors.background,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
paddingHorizontal: 4,
|
|
borderWidth: 1.5,
|
|
borderColor: colors.primary,
|
|
},
|
|
cartBadgeText: {
|
|
fontSize: 10,
|
|
fontWeight: '800',
|
|
lineHeight: 13,
|
|
} as any,
|
|
|
|
/* ── Restricted Access ── */
|
|
restrictedContainer: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
padding: spacing.l,
|
|
backgroundColor: colors.background,
|
|
},
|
|
restrictedCard: {
|
|
backgroundColor: colors.surface,
|
|
borderRadius: 28,
|
|
padding: spacing.l,
|
|
alignItems: 'center',
|
|
width: '100%',
|
|
maxWidth: 400,
|
|
...shadows.strong,
|
|
},
|
|
lockRing: {
|
|
width: 100,
|
|
height: 100,
|
|
borderRadius: 50,
|
|
backgroundColor: `${colors.primary}12`,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
marginBottom: spacing.l,
|
|
borderWidth: 1.5,
|
|
borderColor: `${colors.primary}25`,
|
|
},
|
|
lockInner: {
|
|
width: 72,
|
|
height: 72,
|
|
borderRadius: 36,
|
|
backgroundColor: `${colors.primary}18`,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
restrictedTitle: {
|
|
marginBottom: spacing.s + 4,
|
|
} as any,
|
|
restrictedMessage: {
|
|
marginBottom: spacing.s,
|
|
lineHeight: 24,
|
|
} as any,
|
|
hintBox: {
|
|
backgroundColor: colors.overlay,
|
|
borderRadius: 14,
|
|
paddingHorizontal: spacing.m,
|
|
paddingVertical: spacing.s + 2,
|
|
marginBottom: spacing.l,
|
|
width: '100%',
|
|
},
|
|
loginButton: {
|
|
backgroundColor: colors.buttonPrimary,
|
|
paddingHorizontal: spacing.l,
|
|
paddingVertical: 14,
|
|
borderRadius: 16,
|
|
width: '100%',
|
|
alignItems: 'center',
|
|
...shadows.medium,
|
|
},
|
|
loginBtnText: {
|
|
letterSpacing: 1.2,
|
|
} as any,
|
|
});
|