diff options
| author | Lucas Faria Mendes <lucas.fariamo08@gmail.com> | 2026-08-14 18:06:35 +0000 |
|---|---|---|
| committer | Lucas Faria Mendes <lucas.fariamo08@gmail.com> | 2026-08-14 18:06:35 +0000 |
| commit | 2c2dfba45357bbd205a59145ae55a2779c911ae0 (patch) | |
| tree | 0736b848aed56ec2da3c2489c49da0519447703d /src/components/expense-row.tsx | |
| download | yaba-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.tsx | 213 |
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, + }, +}); |