From 2c2dfba45357bbd205a59145ae55a2779c911ae0 Mon Sep 17 00:00:00 2001 From: Lucas Faria Mendes Date: Fri, 14 Aug 2026 15:06:35 -0300 Subject: remake Signed-off-by: Lucas Faria Mendes --- src/components/animated-splash-screen.tsx | 103 +++++++++++++++ src/components/expense-row.tsx | 213 ++++++++++++++++++++++++++++++ src/components/ui/action-modal.tsx | 107 +++++++++++++++ src/components/ui/button.tsx | 141 ++++++++++++++++++++ src/components/ui/expenses-chart.tsx | 187 ++++++++++++++++++++++++++ src/components/ui/input.tsx | 98 ++++++++++++++ src/components/ui/options-modal.tsx | 137 +++++++++++++++++++ src/components/ui/primitives.tsx | 104 +++++++++++++++ 8 files changed, 1090 insertions(+) create mode 100644 src/components/animated-splash-screen.tsx create mode 100644 src/components/expense-row.tsx create mode 100644 src/components/ui/action-modal.tsx create mode 100644 src/components/ui/button.tsx create mode 100644 src/components/ui/expenses-chart.tsx create mode 100644 src/components/ui/input.tsx create mode 100644 src/components/ui/options-modal.tsx create mode 100644 src/components/ui/primitives.tsx (limited to 'src/components') diff --git a/src/components/animated-splash-screen.tsx b/src/components/animated-splash-screen.tsx new file mode 100644 index 0000000..7ecfdef --- /dev/null +++ b/src/components/animated-splash-screen.tsx @@ -0,0 +1,103 @@ +import React, { useEffect } from 'react'; +import { StyleSheet, View, Text } from 'react-native'; +import Animated, { + useAnimatedStyle, + useSharedValue, + withTiming, + withSpring, + Easing, + runOnJS, + withDelay, +} from 'react-native-reanimated'; +import { Colors } from '@/constants/theme'; +import { LinearGradient } from 'expo-linear-gradient'; + +interface Props { + isAppReady: boolean; + onAnimationComplete: () => void; +} + +export function AnimatedSplashScreen({ isAppReady, onAnimationComplete }: Props) { + const opacity = useSharedValue(1); + const scale = useSharedValue(1); + const textOpacity = useSharedValue(0); + const textTranslateY = useSharedValue(20); + const logoScale = useSharedValue(0.8); + + useEffect(() => { + textOpacity.value = withTiming(1, { duration: 1000 }); + textTranslateY.value = withSpring(0, { damping: 14, stiffness: 100 }); + logoScale.value = withSpring(1, { damping: 10, stiffness: 100 }); + }, []); + + useEffect(() => { + if (isAppReady) { + opacity.value = withDelay( + 600, + withTiming(0, { duration: 700, easing: Easing.inOut(Easing.ease) }, (finished) => { + if (finished) { + runOnJS(onAnimationComplete)(); + } + }) + ); + + scale.value = withDelay( + 600, + withTiming(1.3, { duration: 700, easing: Easing.inOut(Easing.ease) }) + ); + } + }, [isAppReady]); + + const animatedContainerStyle = useAnimatedStyle(() => ({ + opacity: opacity.value, + transform: [{ scale: scale.value }], + })); + + const animatedTextStyle = useAnimatedStyle(() => ({ + opacity: textOpacity.value, + transform: [ + { translateY: textTranslateY.value }, + { scale: logoScale.value } + ], + })); + + return ( + + + + YABA + Finanças Pessoais + + + ); +} + +const styles = StyleSheet.create({ + container: { + ...StyleSheet.absoluteFill, + justifyContent: 'center', + alignItems: 'center', + zIndex: 9999, + }, + logoText: { + fontFamily: 'Outfit_700Bold', + fontSize: 72, + color: Colors.white, + letterSpacing: -2, + textAlign: 'center', + }, + subText: { + fontFamily: 'DMSans_500Medium', + fontSize: 16, + color: Colors.primaryLight, + letterSpacing: 2, + textTransform: 'uppercase', + textAlign: 'center', + marginTop: 8, + } +}); diff --git a/src/components/expense-row.tsx b/src/components/expense-row.tsx new file mode 100644 index 0000000..0f58c78 --- /dev/null +++ b/src/components/expense-row.tsx @@ -0,0 +1,213 @@ +import React from 'react'; +import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import Animated, { FadeInDown, LinearTransition } from 'react-native-reanimated'; +import { Colors, Radius, Spacing, Typography } from '@/constants/theme'; +import type { OneTimeExpense, RecurringExpense } from '@/types'; + +const CATEGORY_EMOJI: Record = { + moradia: '🏠', + alimentação: '🍽️', + transporte: '🚗', + saúde: '💊', + educação: '📚', + lazer: '🎬', + serviços: '⚙️', + outros: '📦', +}; + +interface ExpenseRowProps { + name: string; + amount: number; + category: string; + subtitle?: string; + index?: number; + onPress?: () => void; + rightElement?: React.ReactNode; + type?: 'expense' | 'income'; +} + +export function ExpenseRow({ + name, + amount, + category, + subtitle, + index = 0, + onPress, + rightElement, + type = 'expense', +}: ExpenseRowProps) { + const emoji = CATEGORY_EMOJI[category] ?? '📦'; + const prefix = type === 'income' ? '+ ' : '- '; + const amountColor = type === 'income' ? Colors.positive : Colors.negative; + + const formattedAmount = amount.toLocaleString('pt-BR', { + style: 'currency', + currency: 'BRL', + minimumFractionDigits: 2, + }); + + return ( + + + + {emoji} + + + + + {name} + + {subtitle ? ( + + {subtitle} + + ) : null} + + + + + {prefix}{formattedAmount} + + {rightElement} + + + + ); +} + +interface RecurringExpenseRowProps { + expense: RecurringExpense; + index?: number; + onToggle: () => void; + onPress: () => void; +} + +export function RecurringExpenseRow({ + expense, + index = 0, + onToggle, + onPress, +}: RecurringExpenseRowProps) { + return ( + + + {expense.is_active ? 'Ativo' : 'Pausado'} + + + } + /> + ); +} + +interface OneTimeExpenseRowProps { + expense: OneTimeExpense; + index?: number; + onPress: () => void; +} + +export function OneTimeExpenseRow({ + expense, + index = 0, + onPress, +}: OneTimeExpenseRowProps) { + const date = new Date(expense.expense_date + 'T12:00:00'); + const formattedDate = date.toLocaleDateString('pt-BR', { + day: '2-digit', + month: 'short', + }); + + return ( + + ); +} + +const styles = StyleSheet.create({ + row: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: Spacing.md, + gap: Spacing.md, + }, + iconBox: { + width: 44, + height: 44, + borderRadius: Radius.md, + backgroundColor: Colors.surfaceElevated, + alignItems: 'center', + justifyContent: 'center', + }, + emoji: { + fontSize: 22, + }, + info: { + flex: 1, + gap: 2, + }, + name: { + ...Typography.bodyLarge, + color: Colors.textPrimary, + }, + subtitle: { + ...Typography.caption, + color: Colors.textSecondary, + }, + right: { + alignItems: 'flex-end', + gap: Spacing.xs, + }, + amount: { + ...Typography.bodyLarge, + fontFamily: 'DMSans_600SemiBold', + }, + toggleBadge: { + paddingHorizontal: Spacing.sm, + paddingVertical: 2, + borderRadius: Radius.full, + }, + toggleActive: { + backgroundColor: `${Colors.positive}22`, + }, + toggleInactive: { + backgroundColor: `${Colors.textMuted}22`, + }, + toggleText: { + ...Typography.caption, + color: Colors.textSecondary, + }, +}); diff --git a/src/components/ui/action-modal.tsx b/src/components/ui/action-modal.tsx new file mode 100644 index 0000000..a1d95be --- /dev/null +++ b/src/components/ui/action-modal.tsx @@ -0,0 +1,107 @@ +import React from 'react'; +import { Modal, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import { BlurView } from 'expo-blur'; +import Animated, { FadeIn, FadeInDown, FadeOut, FadeOutDown } from 'react-native-reanimated'; +import { Colors, Radius, Spacing, Typography } from '@/constants/theme'; +import { Button } from './button'; + +interface ActionModalProps { + visible: boolean; + title: string; + message: string; + confirmText?: string; + cancelText?: string; + onConfirm: () => void; + onCancel: () => void; + isDestructive?: boolean; +} + +export function ActionModal({ + visible, + title, + message, + confirmText = 'Confirmar', + cancelText = 'Cancelar', + onConfirm, + onCancel, + isDestructive = false, +}: ActionModalProps) { + if (!visible) return null; + + return ( + + + + + + + + {title} + {message} + + +