Kantongku: aplikasi pencatat keuangan pribadi offline-first
This commit is contained in:
@@ -0,0 +1,552 @@
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { useFocusEffect, useNavigation } from '@react-navigation/native';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Alert, Image, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
|
||||
import { colors } from '../theme/colors';
|
||||
import { formatDateTime, formatRupiah, formatRupiahShort } from '../utils/format';
|
||||
import {
|
||||
deleteSavingsTarget,
|
||||
getSavingsEvents,
|
||||
getSavingsMonthComparison,
|
||||
getSavingsStreak,
|
||||
getSavingsTargets,
|
||||
getSavingsTotal,
|
||||
getSettingNumber,
|
||||
getTransactionCount,
|
||||
} from '../db/repositories';
|
||||
import type { SavingsEvent, SavingsTarget } from '../types';
|
||||
import type { RootStackParamList } from '../navigation';
|
||||
|
||||
interface BadgeDef {
|
||||
icon: string;
|
||||
title: string;
|
||||
desc: string;
|
||||
check: (s: { streak: number; savings: number; txCount: number }) => boolean;
|
||||
}
|
||||
|
||||
const BADGES: BadgeDef[] = [
|
||||
{ icon: '🔥', title: 'Konsisten', desc: 'Nabung 4 minggu berturut', check: (s) => s.streak >= 4 },
|
||||
{ icon: '⚡', title: 'Mesin Nabung', desc: 'Nabung 8 minggu berturut', check: (s) => s.streak >= 8 },
|
||||
{ icon: '🐷', title: 'Pemula', desc: 'Tabungan Rp 100 rb', check: (s) => s.savings >= 100000 },
|
||||
{ icon: '💰', title: 'Aktif', desc: 'Tabungan Rp 500 rb', check: (s) => s.savings >= 500000 },
|
||||
{ icon: '🤑', title: 'Jutawan Kecil', desc: 'Tabungan Rp 1 jt', check: (s) => s.savings >= 1000000 },
|
||||
{ icon: '💎', title: 'Jago Sisih', desc: 'Tabungan Rp 5 jt', check: (s) => s.savings >= 5000000 },
|
||||
{ icon: '📝', title: 'Rajin Catat', desc: '50 transaksi tercatat', check: (s) => s.txCount >= 50 },
|
||||
{ icon: '✍️', title: 'Sultan Catat', desc: '100 transaksi tercatat', check: (s) => s.txCount >= 100 },
|
||||
];
|
||||
|
||||
export function SavingsScreen() {
|
||||
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
|
||||
const [total, setTotal] = useState(0);
|
||||
const [streak, setStreak] = useState(0);
|
||||
const [percent, setPercent] = useState(10);
|
||||
const [events, setEvents] = useState<SavingsEvent[]>([]);
|
||||
const [compare, setCompare] = useState({ current: 0, previous: 0 });
|
||||
const [targets, setTargets] = useState<SavingsTarget[]>([]);
|
||||
const [txCount, setTxCount] = useState(0);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
const [t, s, p, e, c, tg, tx] = await Promise.all([
|
||||
getSavingsTotal(),
|
||||
getSavingsStreak(),
|
||||
getSettingNumber('auto_save_percent', 10),
|
||||
getSavingsEvents(50),
|
||||
getSavingsMonthComparison(),
|
||||
getSavingsTargets(),
|
||||
getTransactionCount(),
|
||||
]);
|
||||
setTotal(t);
|
||||
setStreak(s);
|
||||
setPercent(p);
|
||||
setEvents(e);
|
||||
setCompare(c);
|
||||
setTargets(tg);
|
||||
setTxCount(tx);
|
||||
}, []);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
let active = true;
|
||||
load().then(() => {
|
||||
if (!active) return;
|
||||
});
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [load])
|
||||
);
|
||||
|
||||
const handleDeleteTarget = (target: SavingsTarget) => {
|
||||
Alert.alert(
|
||||
'Hapus target?',
|
||||
`Target "${target.name}" akan dihapus. Tabunganmu tetap aman.`,
|
||||
[
|
||||
{ text: 'Batal', style: 'cancel' },
|
||||
{
|
||||
text: 'Hapus',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
await deleteSavingsTarget(target.id);
|
||||
setTargets(await getSavingsTargets());
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
const badgeStats = { streak, savings: total, txCount };
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
||||
{/* Total tabungan */}
|
||||
<LinearGradient
|
||||
colors={[colors.primary, colors.primaryDark]}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
style={styles.totalCard}
|
||||
>
|
||||
<View style={styles.ornCircleLarge} />
|
||||
<View style={styles.ornCircleSmall} />
|
||||
<Image
|
||||
source={require('../../assets/piggy-3d.png')}
|
||||
style={styles.totalPiggy}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
<View style={styles.totalTop}>
|
||||
<Text style={styles.totalLabel}>TOTAL TABUNGAN</Text>
|
||||
<View style={styles.totalBadge}>
|
||||
<Ionicons name="cash" size={14} color={colors.primaryDark} />
|
||||
</View>
|
||||
</View>
|
||||
<Text style={styles.totalValue}>{formatRupiah(total)}</Text>
|
||||
<Text style={styles.totalHint}>
|
||||
✨ disisihkan otomatis {percent}% dari setiap pemasukan
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
|
||||
{/* Streak */}
|
||||
<LinearGradient
|
||||
colors={['#FFFBEB', '#FEF3C7']}
|
||||
style={[styles.card, styles.streakCard]}
|
||||
>
|
||||
<Ionicons
|
||||
name="flame"
|
||||
size={72}
|
||||
color="rgba(217,119,6,0.12)"
|
||||
style={styles.streakFlame}
|
||||
/>
|
||||
<View style={[styles.streakIconCircle, { backgroundColor: colors.white }]}>
|
||||
<Ionicons name="flame" size={20} color="#D97706" />
|
||||
</View>
|
||||
<View style={styles.streakInfo}>
|
||||
<Text style={[styles.streakValue, { color: '#92400E' }]}>
|
||||
🔥 {streak} minggu berturut-turut
|
||||
</Text>
|
||||
<Text style={styles.streakHint}>
|
||||
{streak === 0
|
||||
? 'Catat pemasukan pertamamu untuk memulai streak!'
|
||||
: 'Pertahankan! Jangan sampai putus minggu ini.'}
|
||||
</Text>
|
||||
</View>
|
||||
</LinearGradient>
|
||||
|
||||
<ComparisonCard current={compare.current} previous={compare.previous} />
|
||||
|
||||
{/* Target Tabungan */}
|
||||
<View style={styles.sectionHeader}>
|
||||
<Text style={styles.sectionTitle}>Target Tabungan</Text>
|
||||
<Pressable onPress={() => navigation.navigate('TargetForm')} hitSlop={8}>
|
||||
<View style={styles.addTargetBtn}>
|
||||
<Ionicons name="add" size={16} color={colors.white} />
|
||||
<Text style={styles.addTargetText}>Tambah</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
{targets.length === 0 ? (
|
||||
<View style={[styles.card, styles.emptyCard]}>
|
||||
<Text style={styles.emptyEmoji}>🎯</Text>
|
||||
<Text style={styles.emptyTitle}>Belum ada target</Text>
|
||||
<Text style={styles.emptyText}>
|
||||
Bikin target nyata (misal Dana Darurat 5 jt) — progress-nya bikin
|
||||
nabung makin semangat!
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
targets.map((target) => {
|
||||
const pct = Math.min(100, Math.round((total / target.targetAmount) * 100));
|
||||
const remaining = Math.max(0, target.targetAmount - total);
|
||||
const now = new Date();
|
||||
const dayOfMonth = now.getDate();
|
||||
const perDay = dayOfMonth > 0 ? compare.current / dayOfMonth : 0;
|
||||
const eta =
|
||||
remaining > 0 && perDay > 0
|
||||
? new Date(now.getTime() + Math.ceil(remaining / perDay) * 86400000)
|
||||
: null;
|
||||
return (
|
||||
<View key={target.id} style={[styles.card, styles.targetCard]}>
|
||||
<View style={styles.targetTop}>
|
||||
<View style={styles.targetTitleRow}>
|
||||
<Text style={styles.targetEmoji}>{target.icon}</Text>
|
||||
<Text style={styles.targetName} numberOfLines={1}>
|
||||
{target.name}
|
||||
</Text>
|
||||
</View>
|
||||
<Pressable onPress={() => handleDeleteTarget(target)} hitSlop={8}>
|
||||
<Ionicons name="trash-outline" size={17} color={colors.textMuted} />
|
||||
</Pressable>
|
||||
</View>
|
||||
<View style={styles.targetTrack}>
|
||||
<View
|
||||
style={[styles.targetFill, { width: `${pct}%` }]}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.targetMeta}>
|
||||
<Text style={styles.targetAmount}>
|
||||
{formatRupiahShort(total)} dari {formatRupiahShort(target.targetAmount)}
|
||||
</Text>
|
||||
<Text style={styles.targetPct}>{pct}%</Text>
|
||||
</View>
|
||||
<Text style={styles.targetEta}>
|
||||
{remaining <= 0
|
||||
? '🎉 Tercapai! Target kelar — bikin target baru!'
|
||||
: eta
|
||||
? `± selesai ${eta.toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
})} dengan ritme saat ini`
|
||||
: 'Catat pemasukan untuk mulai estimasi'}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
})
|
||||
)}
|
||||
|
||||
{/* Pencapaian */}
|
||||
<View style={styles.sectionHeader}>
|
||||
<Text style={styles.sectionTitle}>Pencapaian</Text>
|
||||
</View>
|
||||
<View style={styles.badgeGrid}>
|
||||
{BADGES.map((b) => {
|
||||
const unlocked = b.check(badgeStats);
|
||||
return (
|
||||
<View
|
||||
key={b.title}
|
||||
style={[
|
||||
styles.badgeItem,
|
||||
!unlocked && styles.badgeItemLocked,
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.badgeIcon, !unlocked && styles.badgeIconLocked]}>
|
||||
{b.icon}
|
||||
</Text>
|
||||
<Text style={styles.badgeTitle}>{b.title}</Text>
|
||||
<Text style={styles.badgeDesc}>{b.desc}</Text>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
<Text style={styles.sectionTitle}>Riwayat sisihan</Text>
|
||||
{events.length === 0 ? (
|
||||
<View style={[styles.card, styles.emptyCard]}>
|
||||
<Text style={styles.emptyEmoji}>🐷</Text>
|
||||
<Text style={styles.emptyTitle}>Belum ada sisihan</Text>
|
||||
<Text style={styles.emptyText}>
|
||||
Setiap pemasukan yang kamu catat akan otomatis menyisihkan {percent}% ke
|
||||
sini. Mulai dari catat pemasukan pertamamu!
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
events.map((e) => (
|
||||
<View key={e.id} style={[styles.card, styles.eventRow]}>
|
||||
<View style={styles.eventIcon}>
|
||||
<Ionicons name="add" size={18} color={colors.primaryDark} />
|
||||
</View>
|
||||
<View style={styles.eventInfo}>
|
||||
<Text style={styles.eventName} numberOfLines={1}>
|
||||
{e.transactionNote || 'Pemasukan'}
|
||||
</Text>
|
||||
<Text style={styles.eventDate}>{formatDateTime(e.occurredAt)}</Text>
|
||||
</View>
|
||||
<Text style={styles.eventAmount}>+{formatRupiah(e.amount)}</Text>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
function ComparisonCard({ current, previous }: { current: number; previous: number }) {
|
||||
const pct =
|
||||
previous > 0 ? Math.round(((current - previous) / previous) * 100) : null;
|
||||
|
||||
let icon: React.ComponentProps<typeof Ionicons>['name'] = 'remove';
|
||||
let title = '';
|
||||
let sub = `Bulan ini: ${formatRupiahShort(current)} · Bulan lalu: ${formatRupiahShort(previous)}`;
|
||||
let cardStyle = styles.compareNeutral;
|
||||
let iconColor = colors.textMuted;
|
||||
|
||||
if (current === 0 && previous === 0) {
|
||||
title = 'Belum ada sisihan bulan ini';
|
||||
sub = 'Catat pemasukan untuk memulai! 🚀';
|
||||
} else if (previous === 0) {
|
||||
icon = 'sparkles';
|
||||
iconColor = colors.primaryDark;
|
||||
cardStyle = styles.compareUp;
|
||||
title = 'Bulan ini mulai nabung — hebat! 🎉';
|
||||
sub = `Tabungan bulan ini: ${formatRupiahShort(current)}`;
|
||||
} else if (pct === null || pct === 0) {
|
||||
icon = 'remove';
|
||||
title = 'Stabil, sama seperti bulan lalu';
|
||||
sub = `${sub} — Konsisten itu keren!`;
|
||||
} else if (pct > 0) {
|
||||
icon = 'trending-up';
|
||||
iconColor = colors.income;
|
||||
cardStyle = styles.compareUp;
|
||||
title = `Naik ${pct}% dari bulan lalu — pertahankan! 🔥`;
|
||||
} else {
|
||||
icon = 'trending-down';
|
||||
iconColor = '#D97706';
|
||||
cardStyle = styles.compareDown;
|
||||
title = `Turun ${Math.abs(pct)}% dari bulan lalu`;
|
||||
sub = `${sub} — Santai, bulan ini masih panjang. Yang penting konsisten.`;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.card, styles.compareCard, cardStyle]}>
|
||||
<View style={[styles.compareIconCircle, { backgroundColor: colors.white }]}>
|
||||
<Ionicons name={icon} size={18} color={iconColor} />
|
||||
</View>
|
||||
<View style={styles.compareInfo}>
|
||||
<Text style={styles.compareTitle}>{title}</Text>
|
||||
<Text style={styles.compareSub}>{sub}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: colors.background },
|
||||
content: { padding: 16, paddingBottom: 32 },
|
||||
card: {
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 16,
|
||||
padding: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
shadowColor: '#0F172A',
|
||||
shadowOpacity: 0.04,
|
||||
shadowRadius: 6,
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
elevation: 2,
|
||||
},
|
||||
// Total card
|
||||
totalCard: {
|
||||
marginBottom: 12,
|
||||
borderRadius: 20,
|
||||
padding: 18,
|
||||
overflow: 'hidden',
|
||||
shadowColor: colors.primaryDark,
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 12,
|
||||
shadowOffset: { width: 0, height: 6 },
|
||||
elevation: 6,
|
||||
},
|
||||
ornCircleLarge: {
|
||||
position: 'absolute',
|
||||
right: -50,
|
||||
top: -60,
|
||||
width: 180,
|
||||
height: 180,
|
||||
borderRadius: 90,
|
||||
backgroundColor: 'rgba(255,255,255,0.06)',
|
||||
},
|
||||
ornCircleSmall: {
|
||||
position: 'absolute',
|
||||
left: -24,
|
||||
bottom: -40,
|
||||
width: 100,
|
||||
height: 100,
|
||||
borderRadius: 50,
|
||||
backgroundColor: 'rgba(255,255,255,0.05)',
|
||||
},
|
||||
totalPiggy: {
|
||||
position: 'absolute',
|
||||
right: -52,
|
||||
bottom: -16,
|
||||
width: 186,
|
||||
height: 151,
|
||||
opacity: 0.55,
|
||||
transform: [{ perspective: 600 }, { rotateY: '-18deg' }, { scaleX: -1 }],
|
||||
},
|
||||
totalTop: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingRight: 120,
|
||||
},
|
||||
totalLabel: {
|
||||
fontSize: 11,
|
||||
fontWeight: '700',
|
||||
letterSpacing: 1,
|
||||
color: 'rgba(255,255,255,0.85)',
|
||||
},
|
||||
totalBadge: {
|
||||
backgroundColor: 'rgba(255,255,255,0.9)',
|
||||
borderRadius: 12,
|
||||
padding: 5,
|
||||
},
|
||||
totalValue: {
|
||||
fontSize: 32,
|
||||
fontWeight: '800',
|
||||
color: colors.white,
|
||||
marginTop: 8,
|
||||
paddingRight: 110,
|
||||
},
|
||||
totalHint: { fontSize: 12, color: 'rgba(255,255,255,0.85)', marginTop: 8 },
|
||||
// Streak
|
||||
streakCard: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 12,
|
||||
borderColor: '#FDE68A',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
streakFlame: {
|
||||
position: 'absolute',
|
||||
right: -6,
|
||||
top: -10,
|
||||
},
|
||||
streakIconCircle: {
|
||||
width: 38,
|
||||
height: 38,
|
||||
borderRadius: 19,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: 12,
|
||||
},
|
||||
streakInfo: { flex: 1, paddingRight: 60 },
|
||||
streakValue: { fontSize: 15, fontWeight: '700' },
|
||||
streakHint: { fontSize: 12, color: colors.textMuted, marginTop: 2 },
|
||||
// Comparison
|
||||
compareCard: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 20,
|
||||
},
|
||||
compareUp: { backgroundColor: colors.incomeSoft, borderColor: colors.incomeSoft },
|
||||
compareDown: { backgroundColor: '#FEF3C7', borderColor: '#FEF3C7' },
|
||||
compareNeutral: {},
|
||||
compareIconCircle: {
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 18,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: 12,
|
||||
},
|
||||
compareInfo: { flex: 1 },
|
||||
compareTitle: { fontSize: 14, fontWeight: '700', color: colors.text },
|
||||
compareSub: { fontSize: 12, color: colors.textMuted, marginTop: 2, lineHeight: 17 },
|
||||
// Section
|
||||
sectionHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginTop: 6,
|
||||
marginBottom: 10,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
color: colors.text,
|
||||
marginBottom: 10,
|
||||
marginTop: 6,
|
||||
},
|
||||
addTargetBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
backgroundColor: colors.primary,
|
||||
borderRadius: 999,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
},
|
||||
addTargetText: { color: colors.white, fontWeight: '700', fontSize: 12 },
|
||||
// Target
|
||||
targetCard: { marginBottom: 10 },
|
||||
targetTop: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 10,
|
||||
},
|
||||
targetTitleRow: { flexDirection: 'row', alignItems: 'center', gap: 8, flex: 1 },
|
||||
targetEmoji: { fontSize: 20 },
|
||||
targetName: { fontSize: 15, fontWeight: '700', color: colors.text, flex: 1 },
|
||||
targetTrack: {
|
||||
height: 10,
|
||||
borderRadius: 5,
|
||||
backgroundColor: colors.background,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
targetFill: { height: '100%', borderRadius: 5, backgroundColor: colors.primary },
|
||||
targetMeta: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginTop: 6,
|
||||
},
|
||||
targetAmount: { fontSize: 12, color: colors.textMuted },
|
||||
targetPct: { fontSize: 12, fontWeight: '800', color: colors.primaryDark },
|
||||
targetEta: { fontSize: 11, color: colors.textMuted, marginTop: 4, lineHeight: 15 },
|
||||
// Badge
|
||||
badgeGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginBottom: 16 },
|
||||
badgeItem: {
|
||||
width: '31%',
|
||||
backgroundColor: colors.surface,
|
||||
borderWidth: 1,
|
||||
borderColor: '#BBF7D0',
|
||||
borderRadius: 14,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 6,
|
||||
alignItems: 'center',
|
||||
},
|
||||
badgeItemLocked: { borderColor: colors.border, opacity: 0.45 },
|
||||
badgeIcon: { fontSize: 26, marginBottom: 4 },
|
||||
badgeIconLocked: { opacity: 0.5 },
|
||||
badgeTitle: { fontSize: 12, fontWeight: '700', color: colors.text },
|
||||
badgeDesc: { fontSize: 10, color: colors.textMuted, textAlign: 'center', marginTop: 2 },
|
||||
// Riwayat
|
||||
emptyCard: { alignItems: 'center', paddingVertical: 26, marginBottom: 12 },
|
||||
emptyEmoji: { fontSize: 38, marginBottom: 8 },
|
||||
emptyTitle: { fontSize: 15, fontWeight: '700', color: colors.text },
|
||||
emptyText: {
|
||||
color: colors.textMuted,
|
||||
fontSize: 13,
|
||||
textAlign: 'center',
|
||||
marginTop: 4,
|
||||
lineHeight: 19,
|
||||
},
|
||||
eventRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 12,
|
||||
marginBottom: 8,
|
||||
},
|
||||
eventIcon: {
|
||||
width: 38,
|
||||
height: 38,
|
||||
borderRadius: 19,
|
||||
backgroundColor: colors.primarySoft,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: 12,
|
||||
},
|
||||
eventInfo: { flex: 1 },
|
||||
eventName: { fontSize: 14, fontWeight: '600', color: colors.text },
|
||||
eventDate: { fontSize: 11, color: colors.textMuted, marginTop: 2 },
|
||||
eventAmount: { fontSize: 15, fontWeight: '800', color: colors.primaryDark },
|
||||
});
|
||||
Reference in New Issue
Block a user