learning_ai_invt_trdg/mobile/components/dashboard/PortfolioHeroCard.tsx

220 lines
6.0 KiB
TypeScript

import React, { useEffect } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { useSharedValue, withTiming } from 'react-native-reanimated';
import { Colors, Fonts, FontSize, BorderRadius, Shadows, Spacing } from '@/constants/theme';
import { formatNumber } from '@/utils/format';
import { useTradingData } from '@/providers/TradingDataProvider';
function CountUpValue({ target, prefix, suffix, style, duration = 800 }: {
target: number;
prefix?: string;
suffix?: string;
style: any;
duration?: number;
}) {
const [display, setDisplay] = React.useState('0');
const value = useSharedValue(0);
useEffect(() => {
value.value = withTiming(target, { duration });
}, [target]);
useEffect(() => {
const interval = setInterval(() => {
const current = value.value;
if (Math.abs(current) >= 1000) {
setDisplay(current.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }));
} else {
setDisplay(current.toFixed(2));
}
}, 16);
return () => clearInterval(interval);
}, []);
return (
<Text style={style}>
{prefix}{display}{suffix}
</Text>
);
}
export default function PortfolioHeroCard() {
const { portfolio } = useTradingData();
const positive = portfolio.netPnl >= 0;
return (
<View style={[styles.wrapper, Shadows.card]}>
<LinearGradient
colors={['rgba(20,21,26,0.9)', 'rgba(14,15,18,0.95)']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.card}
>
<View style={styles.accentLine} />
<Text style={styles.label}>PORTFOLIO OVERVIEW</Text>
<CountUpValue
target={Math.abs(portfolio.netPnl)}
prefix={positive ? '+$' : '-$'}
style={styles.heroValue}
/>
<CountUpValue
target={Math.abs(portfolio.netPnlPercent)}
prefix={positive ? '+' : '-'}
suffix="%"
style={styles.heroPercent}
/>
<View style={styles.divider} />
<View style={styles.metricsRow}>
<MetricItem label="TOTAL CAPITAL" value={formatNumber(portfolio.totalCapital)} />
<MetricItem label="DEPLOYED" value={formatNumber(portfolio.deployed)} />
<MetricItem label="AVAILABLE" value={formatNumber(portfolio.available)} />
</View>
<View style={styles.utilizationContainer}>
<View style={styles.utilizationTrack}>
<LinearGradient
colors={['#00ff88', '#00cc6a']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={[styles.utilizationFill, { width: `${portfolio.utilization}%` as any }]}
/>
</View>
<Text style={styles.utilizationText}>{portfolio.utilization.toFixed(1)}% utilized</Text>
</View>
<View style={styles.pnlRow}>
<View style={styles.pnlItem}>
<Text style={styles.pnlLabel}>Realized P&L</Text>
<Text style={[styles.pnlValue, { color: Colors.accent.green }]}>
{portfolio.realizedPnl >= 0 ? '+' : '-'}${Math.abs(portfolio.realizedPnl).toLocaleString('en-US', { minimumFractionDigits: 2 })}
</Text>
</View>
<View style={styles.pnlItem}>
<Text style={styles.pnlLabel}>Unrealized P&L</Text>
<Text style={[styles.pnlValue, { color: Colors.accent.green }]}>
{portfolio.unrealizedPnl >= 0 ? '+' : '-'}${Math.abs(portfolio.unrealizedPnl).toLocaleString('en-US', { minimumFractionDigits: 2 })}
</Text>
</View>
</View>
</LinearGradient>
</View>
);
}
function MetricItem({ label, value }: { label: string; value: string }) {
return (
<View style={styles.metricItem}>
<Text style={styles.metricLabel}>{label}</Text>
<Text style={styles.metricValue}>{value}</Text>
</View>
);
}
const styles = StyleSheet.create({
wrapper: {
borderRadius: BorderRadius.large,
overflow: 'hidden',
},
card: {
borderRadius: BorderRadius.large,
padding: Spacing.cardPaddingLarge,
borderWidth: 1,
borderColor: Colors.border.default,
overflow: 'hidden',
},
accentLine: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: 2,
backgroundColor: Colors.accent.green,
opacity: 0.6,
},
label: {
fontFamily: Fonts.inter.black,
fontSize: FontSize.micro,
color: Colors.accent.green,
letterSpacing: 4,
marginBottom: 16,
},
heroValue: {
fontFamily: Fonts.mono.extraBold,
fontSize: FontSize.pageTitle,
color: Colors.accent.green,
letterSpacing: -0.5,
},
heroPercent: {
fontFamily: Fonts.mono.bold,
fontSize: FontSize.bodyLarge,
color: Colors.accent.green,
marginTop: 2,
},
divider: {
height: 1,
backgroundColor: Colors.border.default,
marginVertical: 16,
},
metricsRow: {
flexDirection: 'row',
justifyContent: 'space-between',
},
metricItem: {
flex: 1,
},
metricLabel: {
fontFamily: Fonts.inter.black,
fontSize: FontSize.micro,
color: Colors.text.secondary,
letterSpacing: 1,
marginBottom: 4,
},
metricValue: {
fontFamily: Fonts.mono.extraBold,
fontSize: FontSize.subheading,
color: Colors.text.primary,
},
utilizationContainer: {
marginTop: 16,
},
utilizationTrack: {
height: 4,
borderRadius: 2,
backgroundColor: Colors.background.elevated,
overflow: 'hidden',
},
utilizationFill: {
height: 4,
borderRadius: 2,
},
utilizationText: {
fontFamily: Fonts.inter.medium,
fontSize: FontSize.micro,
color: Colors.text.secondary,
marginTop: 6,
},
pnlRow: {
flexDirection: 'row',
marginTop: 16,
gap: 24,
},
pnlItem: {
flex: 1,
},
pnlLabel: {
fontFamily: Fonts.inter.medium,
fontSize: FontSize.micro,
color: Colors.text.secondary,
marginBottom: 2,
},
pnlValue: {
fontFamily: Fonts.mono.bold,
fontSize: FontSize.body,
},
});