Fix form keyboard issues and sticky footers in agent screens

This commit is contained in:
Abin Toppo
2026-05-26 16:34:05 +05:30
commit 9ac5e95d52
410 changed files with 86650 additions and 0 deletions
@@ -0,0 +1,42 @@
import React from 'react';
import { StyleSheet, Text, View, Pressable } from 'react-native';
import { useTheme } from '../../../theme';
interface HomeSectionHeaderProps {
title: string;
onSeeAll?: () => void;
}
export const HomeSectionHeader = ({ title, onSeeAll }: HomeSectionHeaderProps) => {
const theme = useTheme();
return (
<View style={styles.container}>
<Text style={[styles.title, { color: theme.colors.textPrimary }]}>{title}</Text>
{onSeeAll && (
<Pressable onPress={onSeeAll}>
<Text style={[styles.seeAll, { color: theme.colors.primary }]}>See All</Text>
</Pressable>
)}
</View>
);
};
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
paddingHorizontal: 4,
},
title: {
fontSize: 18,
fontWeight: '800',
letterSpacing: -0.5,
},
seeAll: {
fontSize: 14,
fontWeight: '700',
},
});
@@ -0,0 +1,113 @@
import React from 'react';
import { StyleSheet, Text, View, ScrollView, Pressable } from 'react-native';
import Ionicons from '@react-native-vector-icons/ionicons';
import { useTheme } from '../../../theme';
interface Category {
id: string;
name: string;
icon: string;
}
interface HorizontalCategoriesProps {
onCategoryPress?: (categoryId: string) => void;
}
const CATEGORIES: Category[] = [
{ id: 'cards', name: 'Cards', icon: 'card-outline' },
{ id: 'deposits', name: 'Deposits', icon: 'wallet-outline' },
{ id: 'loans', name: 'Loans', icon: 'business-outline' },
{ id: 'insurance', name: 'Insurance', icon: 'shield-outline' },
];
export const HorizontalCategories = ({ onCategoryPress }: HorizontalCategoriesProps) => {
const theme = useTheme();
return (
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.container}
>
{CATEGORIES.map((cat) => {
return (
<Pressable
key={cat.id}
onPress={() => onCategoryPress?.(cat.id)}
style={({ pressed }) => [
styles.pill,
{
backgroundColor: pressed ? theme.colors.surfaceMuted : theme.colors.surface,
borderColor: theme.colors.cardBorder,
borderRadius: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: pressed ? 0.08 : 0.05,
shadowRadius: pressed ? 10 : 8,
elevation: pressed ? 4 : 2,
opacity: pressed ? 0.96 : 1,
transform: [{ scale: pressed ? 0.97 : 1 }],
},
]}
>
{({ pressed }) => (
<>
<View
style={[
styles.iconBox,
{
backgroundColor: pressed
? 'rgba(0, 0, 0, 0.08)'
: 'rgba(0, 0, 0, 0.05)',
},
]}
>
<Ionicons
name={cat.icon as any}
size={18}
color={theme.colors.textSecondary}
/>
</View>
<Text
style={[
styles.name,
{ color: theme.colors.textPrimary },
]}
>
{cat.name}
</Text>
</>
)}
</Pressable>
);
})}
</ScrollView>
);
};
const styles = StyleSheet.create({
container: {
paddingHorizontal: 6,
gap: 12,
paddingVertical: 12,
},
pill: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 12,
paddingVertical: 10,
gap: 10,
borderWidth: 1,
},
iconBox: {
width: 32,
height: 32,
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
},
name: {
fontSize: 14,
fontWeight: '700',
},
});
@@ -0,0 +1,107 @@
import React, { memo } from 'react';
import {
StyleSheet,
Text,
View,
} from 'react-native';
import type { InsightMetric, SpendingBreakdown } from '../../../types/app';
import { useTheme } from '../../../theme';
import { formatCompactCurrency } from '../../../utils/formatters';
import { Card } from '../../../components';
interface InsightsCardProps {
insights: InsightMetric[];
spending: SpendingBreakdown[];
}
const InsightsCardComponent = ({ insights, spending }: InsightsCardProps) => {
const theme = useTheme();
const totalSpend = spending.reduce((sum, item) => sum + item.amount, 0);
return (
<Card>
<Text style={[styles.heading, { color: theme.colors.textPrimary }]}>Smart insights</Text>
<View style={styles.metricRow}>
{insights.map((metric) => (
<View key={metric.id} style={styles.metricCell}>
<Text style={[styles.metricLabel, { color: theme.colors.textSecondary }]}>
{metric.label}
</Text>
<Text style={[styles.metricValue, { color: theme.colors.textPrimary }]}>
{formatCompactCurrency(metric.amount)}
</Text>
</View>
))}
</View>
<Text style={[styles.subheading, { color: theme.colors.textSecondary }]}>
Monthly spend mix · {formatCompactCurrency(totalSpend)}
</Text>
<View style={styles.barRow}>
{spending.map((item) => (
<View key={item.id} style={styles.barCell}>
<View
style={[
styles.bar,
{
backgroundColor: item.color,
height: Math.max(24, item.amount / 500),
},
]}
/>
<Text style={[styles.barLabel, { color: theme.colors.textSecondary }]}>{item.label}</Text>
</View>
))}
</View>
</Card>
);
};
const styles = StyleSheet.create({
heading: {
fontSize: 18,
fontWeight: '700',
},
metricRow: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 12,
marginTop: 14,
},
metricCell: {
minWidth: '30%',
gap: 6,
},
metricLabel: {
fontSize: 12,
},
metricValue: {
fontSize: 15,
fontWeight: '700',
},
subheading: {
fontSize: 12,
marginTop: 18,
},
barRow: {
flexDirection: 'row',
alignItems: 'flex-end',
justifyContent: 'space-between',
gap: 12,
marginTop: 14,
},
barCell: {
flex: 1,
alignItems: 'center',
gap: 8,
},
bar: {
width: '100%',
borderRadius: 999,
},
barLabel: {
fontSize: 11,
},
});
export const InsightsCard = memo(InsightsCardComponent);
+135
View File
@@ -0,0 +1,135 @@
import React, { memo } from 'react';
import { StyleSheet, Text, View, Pressable, ImageBackground } from 'react-native';
import LinearGradient from 'react-native-linear-gradient';
import { useTheme } from '../../../theme';
interface PromoBannerProps {
title: string;
subtitle: string;
badge?: string;
interestRate?: string;
onPress?: () => void;
colors?: [string, string, string];
}
const PromoBannerComponent = ({
title,
subtitle,
badge,
interestRate,
onPress,
colors = ['#3B82F6', '#2563EB', '#1D4ED8'],
}: PromoBannerProps) => {
const theme = useTheme();
return (
<Pressable onPress={onPress}>
<LinearGradient
colors={colors}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={[styles.container, { borderRadius: 28, overflow: 'hidden' }]}
>
<View style={styles.topRow}>
{badge && (
<View style={[styles.badge, { backgroundColor: 'rgba(255, 255, 255, 0.2)' }]}>
<Text style={styles.badgeText}>{badge.toUpperCase()}</Text>
</View>
)}
{interestRate && (
<Text style={styles.rateText}>{interestRate} p.a.</Text>
)}
</View>
<View style={styles.content}>
<Text style={styles.title}>{title}</Text>
<Text style={styles.subtitle} numberOfLines={2}>{subtitle}</Text>
</View>
<View style={styles.footer}>
<Text style={styles.ctaText}>Apply Now</Text>
<View style={[styles.arrowCircle, { backgroundColor: 'white' }]}>
<Text style={{ color: colors[1], fontSize: 14 }}>→</Text>
</View>
</View>
{/* Decorative Circles */}
<View style={[styles.circle, { top: -40, right: -40, width: 120, height: 120, opacity: 0.1 }]} />
<View style={[styles.circle, { bottom: -60, left: -20, width: 150, height: 150, opacity: 0.15 }]} />
</LinearGradient>
</Pressable>
);
};
const styles = StyleSheet.create({
container: {
padding: 24,
gap: 16,
position: 'relative',
minHeight: 180,
},
topRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
badge: {
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 8,
},
badgeText: {
color: 'white',
fontSize: 10,
fontWeight: '800',
letterSpacing: 1,
},
rateText: {
color: 'rgba(255, 255, 255, 0.8)',
fontSize: 13,
fontWeight: '700',
},
content: {
gap: 6,
zIndex: 1,
},
title: {
color: 'white',
fontSize: 24,
fontWeight: '900',
letterSpacing: -0.5,
},
subtitle: {
color: 'rgba(255, 255, 255, 0.7)',
fontSize: 14,
fontWeight: '500',
lineHeight: 20,
maxWidth: '80%',
},
footer: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
marginTop: 8,
zIndex: 1,
},
ctaText: {
color: 'white',
fontSize: 15,
fontWeight: '700',
},
arrowCircle: {
width: 24,
height: 24,
borderRadius: 12,
alignItems: 'center',
justifyContent: 'center',
},
circle: {
position: 'absolute',
borderRadius: 100,
backgroundColor: 'white',
},
});
export const PromoBanner = memo(PromoBannerComponent);
@@ -0,0 +1,97 @@
import React, { memo } from 'react';
import {
Pressable,
StyleSheet,
Text,
View,
} from 'react-native';
import Ionicons from '@react-native-vector-icons/ionicons';
import { useTheme } from '../../../theme';
import { Card } from '../../../components';
const CBS_ACTIONS = [
{ id: 'transfer', title: 'Transfer', subtitle: 'Send Funds', icon: 'send', color: '#1E40AF', route: 'Payments' },
{ id: 'payments', title: 'Pay Bills', subtitle: 'Utilities', icon: 'receipt', color: '#10B981', route: 'Payments' },
{ id: 'airtime', title: 'Airtime', subtitle: 'Recharge', icon: 'phone-portrait', color: '#8B5CF6', route: 'Payments' },
{ id: 'loans', title: 'Loans', subtitle: 'Manage', icon: 'cash', color: '#F59E0B', route: 'LoanFlow' },
{ id: 'cards', title: 'Cards', subtitle: 'Security', icon: 'card', color: '#EC4899', route: 'Cards' },
{ id: 'services', title: 'Stats', subtitle: 'Summary', icon: 'stats-chart', color: '#0EA5E9', route: 'Home' },
];
interface QuickActionsProps {
onPressRoute: (route: string) => void;
}
const QuickActionsComponent = ({ onPressRoute }: QuickActionsProps) => {
const theme = useTheme();
return (
<View style={styles.grid}>
{CBS_ACTIONS.map((action) => (
<Card
key={action.id}
style={styles.card}
>
<Pressable
onPress={() => onPressRoute(action.route)}
style={styles.pressable}
>
<View style={[styles.iconContainer, { backgroundColor: `${action.color}15` }]}>
<Ionicons name={action.icon as any} size={22} color={action.color} />
</View>
<View>
<Text style={[styles.title, { color: theme.colors.textPrimary }]}>{action.title}</Text>
<Text style={[styles.subtitle, { color: theme.colors.textSecondary }]}>
{action.subtitle}
</Text>
</View>
</Pressable>
</Card>
))}
</View>
);
};
const styles = StyleSheet.create({
grid: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 12,
},
card: {
width: '31%', // Three columns for a more dense, premium feel
padding: 0,
borderRadius: 20,
overflow: 'hidden',
},
pressable: {
padding: 16,
gap: 10,
alignItems: 'center',
},
iconContainer: {
width: 48,
height: 48,
borderRadius: 14,
alignItems: 'center',
justifyContent: 'center',
},
title: {
fontSize: 13,
fontWeight: '800',
textAlign: 'center',
letterSpacing: -0.2,
},
subtitle: {
fontSize: 9,
marginTop: 2,
fontWeight: '600',
textAlign: 'center',
opacity: 0.6,
textTransform: 'uppercase',
},
});
export const QuickActions = memo(QuickActionsComponent);
@@ -0,0 +1,87 @@
import React, { memo } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import Ionicons from '@react-native-vector-icons/ionicons';
import { useTheme } from '../../../theme';
interface ServiceGridItemProps {
id: string;
title: string;
icon: string;
iconColor?: string;
onPress?: () => void;
fullWidth?: boolean;
}
const ServiceGridItemComponent = ({
title,
icon,
iconColor,
onPress,
fullWidth = false,
}: ServiceGridItemProps) => {
const theme = useTheme();
return (
<Pressable
onPress={onPress}
style={({ pressed }) => [
styles.container,
{
backgroundColor: theme.colors.surface,
borderColor: theme.colors.cardBorder,
borderRadius: 20,
width: fullWidth ? '100%' : '48%',
shadowColor: '#0F172A',
shadowOffset: { width: 0, height: pressed ? 2 : 6 },
shadowOpacity: pressed ? 0.025 : 0.055,
shadowRadius: pressed ? 8 : 14,
elevation: pressed ? 1 : 2,
opacity: pressed ? 0.98 : 1,
transform: [{ scale: pressed ? 0.992 : 1 }, { translateY: pressed ? 1 : 0 }],
},
]}
>
<View
style={[
styles.iconFrame,
{
backgroundColor: `${iconColor || theme.colors.primary}12`,
borderColor: `${iconColor || theme.colors.primary}18`,
},
]}
>
<Ionicons name={icon as any} size={22} color={iconColor || theme.colors.primary} />
</View>
<Text style={[styles.title, { color: theme.colors.textPrimary }]} numberOfLines={2}>
{title}
</Text>
</Pressable>
);
};
const styles = StyleSheet.create({
container: {
minHeight: 112,
justifyContent: 'space-between',
borderWidth: 1,
padding: 15,
gap: 10,
marginBottom: 14,
},
iconFrame: {
width: 44,
height: 44,
borderRadius: 14,
borderWidth: 1,
alignItems: 'center',
justifyContent: 'center',
},
title: {
fontSize: 13,
fontWeight: '700',
letterSpacing: -0.2,
lineHeight: 17,
},
});
export const ServiceGridItem = memo(ServiceGridItemComponent);
+118
View File
@@ -0,0 +1,118 @@
import React, { memo } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import Ionicons from '@react-native-vector-icons/ionicons';
import { useTheme } from '../../../theme';
import { Card } from '../../../components';
interface SummaryCardProps {
branch: string;
user: string;
tenant: string;
}
const SummaryCardComponent = ({ branch, user, tenant }: SummaryCardProps) => {
const theme = useTheme();
const today = new Date().toLocaleDateString('en-IN', {
day: '2-digit',
month: 'short',
year: 'numeric',
});
return (
<Card style={styles.container}>
<View style={styles.header}>
<View style={[styles.iconFrame, { backgroundColor: `${theme.colors.primary}10` }]}>
<Ionicons name="information-circle" size={18} color={theme.colors.primary} />
</View>
<View>
<Text style={[styles.title, { color: theme.colors.textPrimary }]}>Account Details</Text>
<Text style={[styles.date, { color: theme.colors.textSecondary }]}>{today}</Text>
</View>
</View>
<View style={styles.grid}>
<View style={styles.item}>
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>Branch Name</Text>
<Text style={[styles.value, { color: theme.colors.textPrimary }]}>{branch}</Text>
</View>
<View style={styles.item}>
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>Account ID</Text>
<Text style={[styles.value, { color: theme.colors.textPrimary }]}>{user}</Text>
</View>
<View style={styles.item}>
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>Server Node</Text>
<Text style={[styles.value, { color: theme.colors.textPrimary }]}>{tenant}</Text>
</View>
<View style={styles.item}>
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>System Status</Text>
<View style={styles.row}>
<View style={[styles.dot, { backgroundColor: theme.colors.success }]} />
<Text style={[styles.value, { color: theme.colors.success }]}>Online</Text>
</View>
</View>
</View>
</Card>
);
};
const styles = StyleSheet.create({
container: {
gap: 20,
padding: 20,
borderRadius: 24,
},
header: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
},
iconFrame: {
width: 36,
height: 36,
borderRadius: 10,
alignItems: 'center',
justifyContent: 'center',
},
title: {
fontSize: 16,
fontWeight: '800',
letterSpacing: -0.3,
},
date: {
fontSize: 12,
fontWeight: '500',
marginTop: 1,
},
grid: {
flexDirection: 'row',
flexWrap: 'wrap',
rowGap: 16,
},
item: {
width: '50%',
gap: 4,
},
label: {
fontSize: 10,
fontWeight: '700',
textTransform: 'uppercase',
letterSpacing: 1,
},
value: {
fontSize: 13,
fontWeight: '700',
},
row: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
},
dot: {
width: 6,
height: 6,
borderRadius: 3,
},
});
export const SummaryCard = memo(SummaryCardComponent);