aboutsummaryrefslogtreecommitdiff
path: root/src/components/expense-row.tsx
diff options
context:
space:
mode:
authorLucas Faria Mendes <lucas.fariamo08@gmail.com>2026-08-14 18:06:35 +0000
committerLucas Faria Mendes <lucas.fariamo08@gmail.com>2026-08-14 18:06:35 +0000
commit2c2dfba45357bbd205a59145ae55a2779c911ae0 (patch)
tree0736b848aed56ec2da3c2489c49da0519447703d /src/components/expense-row.tsx
downloadyaba-2c2dfba45357bbd205a59145ae55a2779c911ae0.tar.gz
yaba-2c2dfba45357bbd205a59145ae55a2779c911ae0.zip
remake
Signed-off-by: Lucas Faria Mendes <lucas.fariamo08@gmail.com>
Diffstat (limited to 'src/components/expense-row.tsx')
-rw-r--r--src/components/expense-row.tsx213
1 files changed, 213 insertions, 0 deletions
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<string, string> = {
+ 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 (
+ <Animated.View
+ entering={FadeInDown.delay(index * 60).springify()}
+ layout={LinearTransition.springify()}
+ >
+ <TouchableOpacity
+ style={styles.row}
+ onPress={onPress}
+ activeOpacity={0.7}
+ disabled={!onPress}
+ accessibilityRole="button"
+ accessibilityLabel={`${name}, ${formattedAmount}`}
+ >
+ <View style={styles.iconBox}>
+ <Text style={styles.emoji}>{emoji}</Text>
+ </View>
+
+ <View style={styles.info}>
+ <Text style={styles.name} numberOfLines={1}>
+ {name}
+ </Text>
+ {subtitle ? (
+ <Text style={styles.subtitle} numberOfLines={1}>
+ {subtitle}
+ </Text>
+ ) : null}
+ </View>
+
+ <View style={styles.right}>
+ <Text style={[styles.amount, { color: amountColor }]}>
+ {prefix}{formattedAmount}
+ </Text>
+ {rightElement}
+ </View>
+ </TouchableOpacity>
+ </Animated.View>
+ );
+}
+
+interface RecurringExpenseRowProps {
+ expense: RecurringExpense;
+ index?: number;
+ onToggle: () => void;
+ onPress: () => void;
+}
+
+export function RecurringExpenseRow({
+ expense,
+ index = 0,
+ onToggle,
+ onPress,
+}: RecurringExpenseRowProps) {
+ return (
+ <ExpenseRow
+ name={expense.name}
+ amount={expense.amount}
+ category={expense.category}
+ type={expense.type}
+ subtitle={expense.due_day ? `Vence dia ${expense.due_day}` : undefined}
+ index={index}
+ onPress={onPress}
+ rightElement={
+ <TouchableOpacity
+ style={[
+ styles.toggleBadge,
+ expense.is_active ? styles.toggleActive : styles.toggleInactive,
+ ]}
+ onPress={onToggle}
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
+ accessibilityLabel={expense.is_active ? 'Desativar gasto' : 'Ativar gasto'}
+ accessibilityRole="switch"
+ >
+ <Text style={styles.toggleText}>
+ {expense.is_active ? 'Ativo' : 'Pausado'}
+ </Text>
+ </TouchableOpacity>
+ }
+ />
+ );
+}
+
+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 (
+ <ExpenseRow
+ name={expense.name}
+ amount={expense.amount}
+ category={expense.category}
+ subtitle={formattedDate}
+ index={index}
+ onPress={onPress}
+ type={expense.type}
+ />
+ );
+}
+
+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,
+ },
+});