mirror of
https://github.com/arkorty/Expensso.git
synced 2026-03-18 00:47:11 +00:00
feat: beautify the UI
This commit is contained in:
@@ -199,6 +199,10 @@ function makeStyles(theme: MD3Theme) {
|
||||
backgroundColor: colors.surfaceContainerLow,
|
||||
borderTopLeftRadius: shape.extraLarge,
|
||||
borderTopRightRadius: shape.extraLarge,
|
||||
borderTopWidth: 1,
|
||||
borderLeftWidth: 1,
|
||||
borderRightWidth: 1,
|
||||
borderColor: colors.outlineVariant,
|
||||
},
|
||||
handleContainer: {
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -21,7 +21,13 @@ export const SummaryCard: React.FC<SummaryCardProps> = ({
|
||||
}) => {
|
||||
const {colors, typography, elevation, shape, spacing} = useTheme();
|
||||
return (
|
||||
<View style={[styles.card, {backgroundColor: colors.surfaceContainerLow, ...elevation.level1}, style]}>
|
||||
<View style={[styles.card, {
|
||||
backgroundColor: colors.surfaceContainerLow,
|
||||
borderRadius: shape.small,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.outlineVariant,
|
||||
...elevation.level1
|
||||
}, style]}>
|
||||
<View style={styles.cardHeader}>
|
||||
{icon && <View style={styles.iconContainer}>{icon}</View>}
|
||||
<Text style={[styles.cardTitle, {color: colors.onSurfaceVariant}]}>{title}</Text>
|
||||
@@ -36,7 +42,6 @@ export const SummaryCard: React.FC<SummaryCardProps> = ({
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
borderRadius: 16,
|
||||
padding: 16,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {width: 0, height: 2},
|
||||
|
||||
@@ -76,7 +76,7 @@ const styles = StyleSheet.create({
|
||||
iconCircle: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 22,
|
||||
borderRadius: 4,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
|
||||
@@ -187,6 +187,8 @@ function makeStyles(theme: MD3Theme) {
|
||||
flex: 1,
|
||||
backgroundColor: colors.surfaceContainerLow,
|
||||
borderRadius: shape.medium,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.outlineVariant,
|
||||
padding: spacing.lg,
|
||||
...elevation.level1,
|
||||
},
|
||||
@@ -236,6 +238,8 @@ function makeStyles(theme: MD3Theme) {
|
||||
marginTop: spacing.md,
|
||||
backgroundColor: colors.surfaceContainerLow,
|
||||
borderRadius: shape.medium,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.outlineVariant,
|
||||
padding: spacing.lg,
|
||||
...elevation.level1,
|
||||
},
|
||||
|
||||
@@ -4,16 +4,18 @@
|
||||
*/
|
||||
|
||||
import React, {useMemo} from 'react';
|
||||
import {View, Text, StyleSheet} from 'react-native';
|
||||
import {View, Text, StyleSheet, Dimensions} from 'react-native';
|
||||
import {LineChart} from 'react-native-gifted-charts';
|
||||
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import Animated, {FadeInDown} from 'react-native-reanimated';
|
||||
import Animated, {FadeInDown, FadeIn} from 'react-native-reanimated';
|
||||
|
||||
import {useTheme} from '../../theme';
|
||||
import type {MD3Theme} from '../../theme';
|
||||
import {formatCurrency, formatCompact} from '../../utils';
|
||||
import type {Currency, NetWorthSnapshot} from '../../types';
|
||||
|
||||
const {width: SCREEN_WIDTH} = Dimensions.get('window');
|
||||
|
||||
interface NetWorthHeroCardProps {
|
||||
netWorth: number;
|
||||
totalAssets: number;
|
||||
@@ -39,10 +41,26 @@ export const NetWorthHeroCard: React.FC<NetWorthHeroCardProps> = ({
|
||||
}));
|
||||
}, [history]);
|
||||
|
||||
// Calculate trend percentage from history
|
||||
const trendPercentage = useMemo(() => {
|
||||
if (history.length < 2) return null;
|
||||
const latest = history[history.length - 1].netWorth;
|
||||
const previous = history[history.length - 2].netWorth;
|
||||
if (previous === 0) return null;
|
||||
return ((latest - previous) / Math.abs(previous)) * 100;
|
||||
}, [history]);
|
||||
|
||||
const isPositive = netWorth >= 0;
|
||||
const isTrendUp = trendPercentage && trendPercentage > 0;
|
||||
|
||||
return (
|
||||
<Animated.View entering={FadeInDown.duration(500).springify()} style={s.card}>
|
||||
{/* Decorative Background Pattern */}
|
||||
<View style={s.decorativeBackground}>
|
||||
<View style={[s.decorCircle, {backgroundColor: theme.colors.primary + '08'}]} />
|
||||
<View style={[s.decorCircle2, {backgroundColor: theme.colors.tertiary + '06'}]} />
|
||||
</View>
|
||||
|
||||
{/* Sparkline Background */}
|
||||
{sparklineData.length >= 2 && (
|
||||
<View style={s.sparklineContainer}>
|
||||
@@ -55,69 +73,127 @@ export const NetWorthHeroCard: React.FC<NetWorthHeroCardProps> = ({
|
||||
hideAxesAndRules
|
||||
color={
|
||||
theme.isDark
|
||||
? theme.colors.primaryContainer + '40'
|
||||
: theme.colors.primary + '25'
|
||||
? theme.colors.primaryContainer + '60'
|
||||
: theme.colors.primary + '35'
|
||||
}
|
||||
startFillColor={
|
||||
theme.isDark
|
||||
? theme.colors.primaryContainer + '20'
|
||||
: theme.colors.primary + '12'
|
||||
? theme.colors.primaryContainer + '25'
|
||||
: theme.colors.primary + '15'
|
||||
}
|
||||
endFillColor="transparent"
|
||||
thickness={2}
|
||||
width={280}
|
||||
height={100}
|
||||
thickness={2.5}
|
||||
width={SCREEN_WIDTH - 80}
|
||||
height={120}
|
||||
adjustToWidth
|
||||
isAnimated
|
||||
animationDuration={800}
|
||||
initialSpacing={0}
|
||||
endSpacing={0}
|
||||
yAxisOffset={Math.min(...sparklineData.map(d => d.value)) * 0.95}
|
||||
yAxisOffset={Math.min(...sparklineData.map(d => d.value)) * 0.92}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Content Overlay */}
|
||||
<View style={s.content}>
|
||||
<View style={s.labelRow}>
|
||||
<Icon
|
||||
name="chart-line-variant"
|
||||
size={16}
|
||||
color={theme.colors.onSurfaceVariant}
|
||||
/>
|
||||
<Text style={s.label}>NET WORTH</Text>
|
||||
{/* Header with trend badge */}
|
||||
<View style={s.headerRow}>
|
||||
<View style={s.labelRow}>
|
||||
<Icon
|
||||
name="chart-line-variant"
|
||||
size={18}
|
||||
color={theme.colors.onSurfaceVariant}
|
||||
/>
|
||||
<Text style={s.label}>NET WORTH</Text>
|
||||
</View>
|
||||
{trendPercentage !== null && (
|
||||
<Animated.View entering={FadeIn.delay(300)} style={[s.trendBadge, {
|
||||
backgroundColor: isTrendUp
|
||||
? theme.colors.successContainer
|
||||
: theme.colors.errorContainer,
|
||||
}]}>
|
||||
<Icon
|
||||
name={isTrendUp ? 'trending-up' : 'trending-down'}
|
||||
size={14}
|
||||
color={isTrendUp ? theme.colors.success : theme.colors.error}
|
||||
/>
|
||||
<Text style={[s.trendText, {
|
||||
color: isTrendUp ? theme.colors.success : theme.colors.error,
|
||||
}]}>
|
||||
{Math.abs(trendPercentage).toFixed(1)}%
|
||||
</Text>
|
||||
</Animated.View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Main Value */}
|
||||
<Text
|
||||
style={[
|
||||
s.value,
|
||||
{color: isPositive ? theme.colors.success : theme.colors.error},
|
||||
{color: isPositive ? theme.colors.onSurface : theme.colors.error},
|
||||
]}
|
||||
numberOfLines={1}
|
||||
adjustsFontSizeToFit>
|
||||
{formatCurrency(netWorth, currency)}
|
||||
</Text>
|
||||
|
||||
{/* Asset / Liability Split */}
|
||||
<View style={s.splitRow}>
|
||||
<View style={s.splitItem}>
|
||||
<View style={[s.splitDot, {backgroundColor: theme.colors.success}]} />
|
||||
<View>
|
||||
<Text style={s.splitLabel}>Assets</Text>
|
||||
<Text style={[s.splitValue, {color: theme.colors.success}]}>
|
||||
{formatCompact(totalAssets, currency)}
|
||||
</Text>
|
||||
{/* Asset / Liability Split - Enhanced */}
|
||||
<View style={s.splitContainer}>
|
||||
<Animated.View entering={FadeIn.delay(200)} style={s.splitRow}>
|
||||
<View style={s.splitItem}>
|
||||
<View style={[s.iconWrapper, {
|
||||
backgroundColor: theme.colors.successContainer,
|
||||
}]}>
|
||||
<Icon
|
||||
name="trending-up"
|
||||
size={16}
|
||||
color={theme.colors.success}
|
||||
/>
|
||||
</View>
|
||||
<View style={s.splitTextContainer}>
|
||||
<Text style={s.splitLabel}>Assets</Text>
|
||||
<Text style={[s.splitValue, {color: theme.colors.success}]}>
|
||||
{formatCompact(totalAssets, currency)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View style={s.splitDivider} />
|
||||
<View style={s.splitItem}>
|
||||
<View style={[s.splitDot, {backgroundColor: theme.colors.error}]} />
|
||||
<View>
|
||||
<Text style={s.splitLabel}>Liabilities</Text>
|
||||
<Text style={[s.splitValue, {color: theme.colors.error}]}>
|
||||
{formatCompact(totalLiabilities, currency)}
|
||||
</Text>
|
||||
|
||||
<View style={s.splitDivider} />
|
||||
|
||||
<View style={s.splitItem}>
|
||||
<View style={[s.iconWrapper, {
|
||||
backgroundColor: theme.colors.errorContainer,
|
||||
}]}>
|
||||
<Icon
|
||||
name="trending-down"
|
||||
size={16}
|
||||
color={theme.colors.error}
|
||||
/>
|
||||
</View>
|
||||
<View style={s.splitTextContainer}>
|
||||
<Text style={s.splitLabel}>Liabilities</Text>
|
||||
<Text style={[s.splitValue, {color: theme.colors.error}]}>
|
||||
{formatCompact(totalLiabilities, currency)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Animated.View>
|
||||
|
||||
{/* Visual Progress Bar */}
|
||||
<View style={s.progressBar}>
|
||||
<View
|
||||
style={[s.progressAsset, {
|
||||
flex: totalAssets || 1,
|
||||
backgroundColor: theme.colors.success,
|
||||
}]}
|
||||
/>
|
||||
<View
|
||||
style={[s.progressLiability, {
|
||||
flex: totalLiabilities || 0.1,
|
||||
backgroundColor: theme.colors.error,
|
||||
}]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -131,70 +207,143 @@ function makeStyles(theme: MD3Theme) {
|
||||
card: {
|
||||
marginHorizontal: spacing.xl,
|
||||
marginTop: spacing.md,
|
||||
borderRadius: shape.extraLarge,
|
||||
borderRadius: shape.medium,
|
||||
borderWidth: 1.5,
|
||||
borderColor: colors.outlineVariant,
|
||||
backgroundColor: colors.surfaceContainerLow,
|
||||
overflow: 'hidden',
|
||||
...elevation.level3,
|
||||
},
|
||||
decorativeBackground: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
decorCircle: {
|
||||
position: 'absolute',
|
||||
width: 200,
|
||||
height: 200,
|
||||
borderRadius: 100,
|
||||
top: -60,
|
||||
right: -40,
|
||||
},
|
||||
decorCircle2: {
|
||||
position: 'absolute',
|
||||
width: 150,
|
||||
height: 150,
|
||||
borderRadius: 75,
|
||||
bottom: -30,
|
||||
left: -20,
|
||||
},
|
||||
sparklineContainer: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
opacity: 0.6,
|
||||
opacity: 0.7,
|
||||
overflow: 'hidden',
|
||||
borderRadius: shape.extraLarge,
|
||||
},
|
||||
content: {
|
||||
padding: spacing.xxl,
|
||||
paddingTop: spacing.xl,
|
||||
paddingBottom: spacing.lg,
|
||||
},
|
||||
headerRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
labelRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.xs,
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
label: {
|
||||
...typography.labelSmall,
|
||||
...typography.labelMedium,
|
||||
color: colors.onSurfaceVariant,
|
||||
letterSpacing: 1.5,
|
||||
letterSpacing: 1.2,
|
||||
fontWeight: '600',
|
||||
},
|
||||
trendBadge: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
paddingHorizontal: spacing.sm,
|
||||
paddingVertical: 4,
|
||||
borderRadius: shape.small,
|
||||
},
|
||||
trendText: {
|
||||
...typography.labelSmall,
|
||||
fontWeight: '700',
|
||||
},
|
||||
value: {
|
||||
...typography.displaySmall,
|
||||
fontWeight: '700',
|
||||
fontWeight: '800',
|
||||
marginBottom: spacing.lg,
|
||||
marginTop: spacing.xs,
|
||||
letterSpacing: -0.5,
|
||||
},
|
||||
splitContainer: {
|
||||
gap: spacing.md,
|
||||
},
|
||||
splitRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.surfaceContainer,
|
||||
borderRadius: shape.medium,
|
||||
padding: spacing.md,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.outlineVariant + '50',
|
||||
padding: spacing.lg,
|
||||
},
|
||||
splitItem: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.sm,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
splitDot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
iconWrapper: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: shape.small,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
splitTextContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
splitLabel: {
|
||||
...typography.labelSmall,
|
||||
color: colors.onSurfaceVariant,
|
||||
marginBottom: 2,
|
||||
},
|
||||
splitValue: {
|
||||
...typography.titleSmall,
|
||||
...typography.titleMedium,
|
||||
fontWeight: '700',
|
||||
},
|
||||
splitDivider: {
|
||||
width: 1,
|
||||
height: 28,
|
||||
height: 36,
|
||||
backgroundColor: colors.outlineVariant,
|
||||
marginHorizontal: spacing.xs,
|
||||
},
|
||||
progressBar: {
|
||||
flexDirection: 'row',
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: colors.surfaceContainerHighest,
|
||||
},
|
||||
progressAsset: {
|
||||
borderTopLeftRadius: 3,
|
||||
borderBottomLeftRadius: 3,
|
||||
},
|
||||
progressLiability: {
|
||||
borderTopRightRadius: 3,
|
||||
borderBottomRightRadius: 3,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ function makeStyles(theme: MD3Theme) {
|
||||
iconCircle: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
borderRadius: 4,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
|
||||
@@ -126,6 +126,8 @@ function makeStyles(theme: MD3Theme) {
|
||||
marginTop: spacing.xl,
|
||||
backgroundColor: colors.surfaceContainerLow,
|
||||
borderRadius: shape.large,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.outlineVariant,
|
||||
padding: spacing.xl,
|
||||
...elevation.level1,
|
||||
},
|
||||
@@ -164,7 +166,7 @@ function makeStyles(theme: MD3Theme) {
|
||||
legendDot: {
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: 5,
|
||||
borderRadius: 2,
|
||||
},
|
||||
legendText: {
|
||||
flex: 1,
|
||||
|
||||
Reference in New Issue
Block a user