Fix form keyboard issues and sticky footers in agent screens
This commit is contained in:
@@ -0,0 +1,301 @@
|
||||
import React, { memo } from 'react';
|
||||
import {
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import Ionicons from '@react-native-vector-icons/ionicons';
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
import Animated, {
|
||||
Extrapolation,
|
||||
interpolate,
|
||||
SharedValue,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated';
|
||||
|
||||
import type { BankCard } from '../../../types/app';
|
||||
import { getCardStatusLabel } from '../utils/cardCopy';
|
||||
|
||||
interface CardItemProps {
|
||||
card: BankCard;
|
||||
index: number;
|
||||
scrollX: SharedValue<number>;
|
||||
cardWidth: number;
|
||||
itemSize: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
const CardItemComponent = ({
|
||||
card,
|
||||
index,
|
||||
scrollX,
|
||||
cardWidth,
|
||||
itemSize,
|
||||
height = 220,
|
||||
}: CardItemProps) => {
|
||||
const flipProgress = useSharedValue(0);
|
||||
const statusLabel = getCardStatusLabel(card);
|
||||
const statusColor =
|
||||
card.status === 'blocked'
|
||||
? '#FFB3BC'
|
||||
: card.controls.frozen
|
||||
? '#FFE08C'
|
||||
: '#C8FACC';
|
||||
|
||||
const toggleFlip = () => {
|
||||
flipProgress.value = withTiming(flipProgress.value === 0 ? 1 : 0, { duration: 420 });
|
||||
};
|
||||
|
||||
const carouselStyle = useAnimatedStyle(() => {
|
||||
const inputRange = [(index - 1) * itemSize, index * itemSize, (index + 1) * itemSize];
|
||||
|
||||
return {
|
||||
transform: [
|
||||
{
|
||||
scale: interpolate(scrollX.value, inputRange, [0.92, 1, 0.92], Extrapolation.CLAMP),
|
||||
},
|
||||
{
|
||||
translateY: interpolate(scrollX.value, inputRange, [12, 0, 12], Extrapolation.CLAMP),
|
||||
},
|
||||
],
|
||||
opacity: interpolate(scrollX.value, inputRange, [0.72, 1, 0.72], Extrapolation.CLAMP),
|
||||
};
|
||||
});
|
||||
|
||||
const frontStyle = useAnimatedStyle(() => ({
|
||||
transform: [
|
||||
{ perspective: 1200 },
|
||||
{ rotateY: `${interpolate(flipProgress.value, [0, 1], [0, 180])}deg` },
|
||||
],
|
||||
}));
|
||||
|
||||
const backStyle = useAnimatedStyle(() => ({
|
||||
transform: [
|
||||
{ perspective: 1200 },
|
||||
{ rotateY: `${interpolate(flipProgress.value, [0, 1], [180, 360])}deg` },
|
||||
],
|
||||
}));
|
||||
|
||||
return (
|
||||
<Animated.View style={[styles.frame, carouselStyle, { width: cardWidth, height }]}>
|
||||
<Pressable style={styles.flex} onPress={toggleFlip}>
|
||||
<Animated.View style={[styles.face, frontStyle]}>
|
||||
<LinearGradient colors={card.themeColors} style={styles.gradient}>
|
||||
<View style={styles.headerRow}>
|
||||
<View>
|
||||
<Text style={styles.brandName}>CBS Bank</Text>
|
||||
<Text style={styles.cardMeta}>{card.type.toUpperCase()} CARD</Text>
|
||||
</View>
|
||||
<View style={styles.statusBadge}>
|
||||
<View style={[styles.statusDot, { backgroundColor: statusColor }]} />
|
||||
<Text style={styles.statusBadgeText}>{statusLabel}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.centerBlock}>
|
||||
<Text style={styles.cardName}>{card.name}</Text>
|
||||
<Text style={styles.cardNumber}>{card.maskedNumber}</Text>
|
||||
<Text style={styles.holderName}>{card.cardholderName}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.footerRow}>
|
||||
<View>
|
||||
<Text style={styles.footerLabel}>VALID THRU</Text>
|
||||
<Text style={styles.footerValue}>{card.expiry}</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Text style={styles.footerLabel}>NETWORK</Text>
|
||||
<Text style={styles.footerValue}>{card.network}</Text>
|
||||
</View>
|
||||
<View style={styles.flipHint}>
|
||||
<Ionicons name="sync-outline" size={14} color="rgba(255,255,255,0.85)" />
|
||||
<Text style={styles.flipText}>Flip</Text>
|
||||
</View>
|
||||
</View>
|
||||
</LinearGradient>
|
||||
</Animated.View>
|
||||
|
||||
<Animated.View style={[styles.face, styles.backFace, backStyle]}>
|
||||
<LinearGradient
|
||||
colors={[card.themeColors[2], card.themeColors[1], card.themeColors[0]]}
|
||||
style={styles.gradient}
|
||||
>
|
||||
<View style={styles.magneticStrip} />
|
||||
<View style={styles.backSection}>
|
||||
<Text style={styles.backLabel}>PRIMARY LINKED ACCOUNT</Text>
|
||||
<Text style={styles.backValue}>{card.primaryLinkedAccountId}</Text>
|
||||
</View>
|
||||
<View style={styles.backGrid}>
|
||||
<View style={styles.backCell}>
|
||||
<Text style={styles.backLabel}>DAILY LIMIT</Text>
|
||||
<Text style={styles.backValue}>Rs {card.limits.daily.toLocaleString('en-IN')}</Text>
|
||||
</View>
|
||||
<View style={styles.backCell}>
|
||||
<Text style={styles.backLabel}>LINKED ACCOUNTS</Text>
|
||||
<Text style={styles.backValue}>{card.linkedAccountIds.length}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.backFooter}>
|
||||
<Text style={styles.backSupport}>24x7 card care · 1800 100 2244</Text>
|
||||
<Text style={styles.backSupport}>Tap card again to return</Text>
|
||||
</View>
|
||||
</LinearGradient>
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
frame: {
|
||||
overflow: 'visible',
|
||||
},
|
||||
face: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backfaceVisibility: 'hidden',
|
||||
},
|
||||
backFace: {
|
||||
zIndex: 2,
|
||||
},
|
||||
gradient: {
|
||||
flex: 1,
|
||||
borderRadius: 28,
|
||||
padding: 22,
|
||||
justifyContent: 'space-between',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
headerRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
gap: 12,
|
||||
},
|
||||
brandName: {
|
||||
color: 'white',
|
||||
fontSize: 18,
|
||||
fontWeight: '900',
|
||||
letterSpacing: -0.3,
|
||||
},
|
||||
cardMeta: {
|
||||
color: 'rgba(255,255,255,0.72)',
|
||||
fontSize: 10,
|
||||
fontWeight: '800',
|
||||
letterSpacing: 1.1,
|
||||
marginTop: 2,
|
||||
},
|
||||
statusBadge: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
borderRadius: 999,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
backgroundColor: 'rgba(255,255,255,0.14)',
|
||||
},
|
||||
statusDot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
},
|
||||
statusBadgeText: {
|
||||
color: 'white',
|
||||
fontSize: 11,
|
||||
fontWeight: '700',
|
||||
},
|
||||
centerBlock: {
|
||||
gap: 10,
|
||||
},
|
||||
cardName: {
|
||||
color: 'rgba(255,255,255,0.82)',
|
||||
fontSize: 12,
|
||||
fontWeight: '700',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 1,
|
||||
},
|
||||
cardNumber: {
|
||||
color: 'white',
|
||||
fontSize: 28,
|
||||
fontWeight: '900',
|
||||
letterSpacing: 2,
|
||||
},
|
||||
holderName: {
|
||||
color: 'rgba(255,255,255,0.92)',
|
||||
fontSize: 14,
|
||||
fontWeight: '700',
|
||||
letterSpacing: 0.6,
|
||||
},
|
||||
footerRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-end',
|
||||
gap: 12,
|
||||
},
|
||||
footerLabel: {
|
||||
color: 'rgba(255,255,255,0.68)',
|
||||
fontSize: 9,
|
||||
fontWeight: '800',
|
||||
letterSpacing: 1,
|
||||
},
|
||||
footerValue: {
|
||||
color: 'white',
|
||||
fontSize: 13,
|
||||
fontWeight: '800',
|
||||
marginTop: 4,
|
||||
},
|
||||
flipHint: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
},
|
||||
flipText: {
|
||||
color: 'rgba(255,255,255,0.9)',
|
||||
fontSize: 12,
|
||||
fontWeight: '700',
|
||||
},
|
||||
magneticStrip: {
|
||||
height: 42,
|
||||
marginHorizontal: -22,
|
||||
backgroundColor: 'rgba(7, 19, 33, 0.8)',
|
||||
},
|
||||
backSection: {
|
||||
gap: 6,
|
||||
},
|
||||
backGrid: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
},
|
||||
backCell: {
|
||||
flex: 1,
|
||||
gap: 6,
|
||||
padding: 14,
|
||||
borderRadius: 16,
|
||||
backgroundColor: 'rgba(255,255,255,0.12)',
|
||||
},
|
||||
backLabel: {
|
||||
color: 'rgba(255,255,255,0.68)',
|
||||
fontSize: 10,
|
||||
fontWeight: '800',
|
||||
letterSpacing: 0.9,
|
||||
},
|
||||
backValue: {
|
||||
color: 'white',
|
||||
fontSize: 14,
|
||||
fontWeight: '800',
|
||||
},
|
||||
backFooter: {
|
||||
gap: 6,
|
||||
},
|
||||
backSupport: {
|
||||
color: 'rgba(255,255,255,0.82)',
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
||||
export const CardItem = memo(CardItemComponent);
|
||||
Reference in New Issue
Block a user