feat: Introduce financial goal and asset management with SQLite and NativeWind integration, new tab screens, and updated dependencies.
parent
60360e8eb9
commit
e002d0d1fd
|
|
@ -1,11 +1,11 @@
|
||||||
import React from 'react';
|
|
||||||
import FontAwesome from '@expo/vector-icons/FontAwesome';
|
import FontAwesome from '@expo/vector-icons/FontAwesome';
|
||||||
import { Link, Tabs } from 'expo-router';
|
import { Link, Tabs } from 'expo-router';
|
||||||
|
import React from 'react';
|
||||||
import { Pressable } from 'react-native';
|
import { Pressable } from 'react-native';
|
||||||
|
|
||||||
import Colors from '@/constants/Colors';
|
|
||||||
import { useColorScheme } from '@/components/useColorScheme';
|
|
||||||
import { useClientOnlyValue } from '@/components/useClientOnlyValue';
|
import { useClientOnlyValue } from '@/components/useClientOnlyValue';
|
||||||
|
import { useColorScheme } from '@/components/useColorScheme';
|
||||||
|
import Colors from '@/constants/Colors';
|
||||||
|
|
||||||
// You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/
|
// You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/
|
||||||
function TabBarIcon(props: {
|
function TabBarIcon(props: {
|
||||||
|
|
@ -29,14 +29,14 @@ export default function TabLayout() {
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="index"
|
name="index"
|
||||||
options={{
|
options={{
|
||||||
title: 'Tab One',
|
title: 'Dashboard',
|
||||||
tabBarIcon: ({ color }) => <TabBarIcon name="code" color={color} />,
|
tabBarIcon: ({ color }) => <TabBarIcon name="dashboard" color={color} />,
|
||||||
headerRight: () => (
|
headerRight: () => (
|
||||||
<Link href="/modal" asChild>
|
<Link href="/modal" asChild>
|
||||||
<Pressable>
|
<Pressable>
|
||||||
{({ pressed }) => (
|
{({ pressed }) => (
|
||||||
<FontAwesome
|
<FontAwesome
|
||||||
name="info-circle"
|
name="plus-circle"
|
||||||
size={25}
|
size={25}
|
||||||
color={Colors[colorScheme ?? 'light'].text}
|
color={Colors[colorScheme ?? 'light'].text}
|
||||||
style={{ marginRight: 15, opacity: pressed ? 0.5 : 1 }}
|
style={{ marginRight: 15, opacity: pressed ? 0.5 : 1 }}
|
||||||
|
|
@ -48,10 +48,45 @@ export default function TabLayout() {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="two"
|
name="investments"
|
||||||
options={{
|
options={{
|
||||||
title: 'Tab Two',
|
title: 'Investments',
|
||||||
tabBarIcon: ({ color }) => <TabBarIcon name="code" color={color} />,
|
tabBarIcon: ({ color }) => <TabBarIcon name="line-chart" color={color} />,
|
||||||
|
headerRight: () => (
|
||||||
|
<Link href="/add-asset" asChild>
|
||||||
|
<Pressable>
|
||||||
|
{({ pressed }) => (
|
||||||
|
<FontAwesome
|
||||||
|
name="plus-circle"
|
||||||
|
size={25}
|
||||||
|
color={Colors[colorScheme ?? 'light'].text}
|
||||||
|
style={{ marginRight: 15, opacity: pressed ? 0.5 : 1 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Pressable>
|
||||||
|
</Link>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tabs.Screen
|
||||||
|
name="goals"
|
||||||
|
options={{
|
||||||
|
title: 'Goals',
|
||||||
|
tabBarIcon: ({ color }) => <TabBarIcon name="flag" color={color} />,
|
||||||
|
headerRight: () => (
|
||||||
|
<Link href="/add-goal" asChild>
|
||||||
|
<Pressable>
|
||||||
|
{({ pressed }) => (
|
||||||
|
<FontAwesome
|
||||||
|
name="plus-circle"
|
||||||
|
size={25}
|
||||||
|
color={Colors[colorScheme ?? 'light'].text}
|
||||||
|
style={{ marginRight: 15, opacity: pressed ? 0.5 : 1 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Pressable>
|
||||||
|
</Link>
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { useFocusEffect } from 'expo-router';
|
||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import { ScrollView, Text, View } from 'react-native';
|
||||||
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
import { getDB } from '../../db';
|
||||||
|
import { Goal } from '../../types';
|
||||||
|
|
||||||
|
export default function GoalsScreen() {
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
const [goals, setGoals] = useState<Goal[]>([]);
|
||||||
|
|
||||||
|
const fetchData = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const db = getDB();
|
||||||
|
const result = await db.getAllAsync<Goal>('SELECT * FROM goals ORDER BY deadline ASC');
|
||||||
|
setGoals(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching goals', error);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
fetchData();
|
||||||
|
}, [fetchData])
|
||||||
|
);
|
||||||
|
|
||||||
|
const calculateProgress = (current: number, target: number) => {
|
||||||
|
if (target === 0) return 0;
|
||||||
|
return Math.min((current / target) * 100, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="flex-1 bg-gray-100 dark:bg-gray-900" style={{ paddingTop: insets.top }}>
|
||||||
|
<ScrollView className="flex-1 px-6">
|
||||||
|
<View className="py-6">
|
||||||
|
<Text className="text-3xl font-bold text-gray-900 dark:text-white">Financial Goals</Text>
|
||||||
|
<Text className="text-gray-500 mt-1">Stay disciplined and reach your targets.</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{goals.length === 0 ? (
|
||||||
|
<View className="items-center py-10">
|
||||||
|
<Text className="text-gray-400">No goals set yet.</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
goals.map((item) => {
|
||||||
|
const progress = calculateProgress(item.current_amount, item.target_amount);
|
||||||
|
return (
|
||||||
|
<View key={item.id} className="bg-white dark:bg-gray-800 p-6 mb-4 rounded-2xl shadow-sm">
|
||||||
|
<View className="flex-row justify-between items-start mb-4">
|
||||||
|
<View>
|
||||||
|
<Text className="text-xl font-bold text-gray-900 dark:text-white">{item.name}</Text>
|
||||||
|
{item.deadline && (
|
||||||
|
<Text className="text-gray-500 text-xs">Deadline: {item.deadline}</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<View className="bg-blue-100 px-3 py-1 rounded-full">
|
||||||
|
<Text className="text-blue-600 font-bold text-xs">{progress.toFixed(0)}%</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="flex-row justify-between mb-2">
|
||||||
|
<Text className="text-gray-500 text-sm">€{item.current_amount.toFixed(2)} saved</Text>
|
||||||
|
<Text className="text-gray-900 dark:text-white font-bold text-sm">Target: €{item.target_amount.toFixed(2)}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Progress Bar */}
|
||||||
|
<View className="h-3 bg-gray-100 dark:bg-gray-700 rounded-full overflow-hidden">
|
||||||
|
<View
|
||||||
|
className="h-full bg-blue-600 rounded-full"
|
||||||
|
style={{ width: `${progress}%` }}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,31 +1,129 @@
|
||||||
import { StyleSheet } from 'react-native';
|
import { FontAwesome } from '@expo/vector-icons';
|
||||||
|
import { Link, useFocusEffect } from 'expo-router';
|
||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import { ScrollView, Text, TouchableOpacity, View } from 'react-native';
|
||||||
|
import { BarChart } from 'react-native-gifted-charts';
|
||||||
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
import TransactionList from '../../components/TransactionList';
|
||||||
|
import { getDB } from '../../db';
|
||||||
|
import { Transaction } from '../../types';
|
||||||
|
|
||||||
import EditScreenInfo from '@/components/EditScreenInfo';
|
export default function DashboardScreen() {
|
||||||
import { Text, View } from '@/components/Themed';
|
const insets = useSafeAreaInsets();
|
||||||
|
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
||||||
|
const [balance, setBalance] = useState(0);
|
||||||
|
const [income, setIncome] = useState(0);
|
||||||
|
const [expense, setExpense] = useState(0);
|
||||||
|
const [chartData, setChartData] = useState<any[]>([]);
|
||||||
|
|
||||||
|
const fetchData = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const db = getDB();
|
||||||
|
const result = await db.getAllAsync<Transaction>('SELECT * FROM transactions ORDER BY date DESC');
|
||||||
|
setTransactions(result);
|
||||||
|
|
||||||
|
let totalIncome = 0;
|
||||||
|
let totalExpense = 0;
|
||||||
|
const expensesByCategory: Record<string, number> = {};
|
||||||
|
|
||||||
|
result.forEach(t => {
|
||||||
|
if (t.type === 'income') {
|
||||||
|
totalIncome += t.amount;
|
||||||
|
} else {
|
||||||
|
totalExpense += t.amount;
|
||||||
|
expensesByCategory[t.category] = (expensesByCategory[t.category] || 0) + t.amount;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setIncome(totalIncome);
|
||||||
|
setExpense(totalExpense);
|
||||||
|
setBalance(totalIncome - totalExpense);
|
||||||
|
|
||||||
|
// Prepare Chart Data
|
||||||
|
const data = Object.keys(expensesByCategory).map(cat => ({
|
||||||
|
value: expensesByCategory[cat],
|
||||||
|
label: cat.substring(0, 3), // Short label
|
||||||
|
frontColor: '#EF4444',
|
||||||
|
}));
|
||||||
|
setChartData(data);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching data', error);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
fetchData();
|
||||||
|
}, [fetchData])
|
||||||
|
);
|
||||||
|
|
||||||
export default function TabOneScreen() {
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View className="flex-1 bg-gray-100 dark:bg-gray-900" style={{ paddingTop: insets.top }}>
|
||||||
<Text style={styles.title}>Tab One</Text>
|
<ScrollView className="flex-1 px-6">
|
||||||
<View style={styles.separator} lightColor="#eee" darkColor="rgba(255,255,255,0.1)" />
|
{/* Header */}
|
||||||
<EditScreenInfo path="app/(tabs)/index.tsx" />
|
<View className="flex-row justify-between items-center py-6">
|
||||||
|
<View>
|
||||||
|
<Text className="text-gray-500 text-sm">Total Balance</Text>
|
||||||
|
<Text className="text-4xl font-bold text-gray-900 dark:text-white">€{balance.toFixed(2)}</Text>
|
||||||
|
</View>
|
||||||
|
<Link href="/modal" asChild>
|
||||||
|
<TouchableOpacity className="bg-blue-600 w-12 h-12 rounded-full items-center justify-center shadow-lg">
|
||||||
|
<FontAwesome name="plus" size={20} color="white" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</Link>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Summary Cards */}
|
||||||
|
<View className="flex-row space-x-4 mb-8">
|
||||||
|
<View className="flex-1 bg-green-500 p-4 rounded-2xl shadow-sm">
|
||||||
|
<View className="flex-row items-center mb-2">
|
||||||
|
<View className="bg-white/20 w-8 h-8 rounded-full items-center justify-center mr-2">
|
||||||
|
<FontAwesome name="arrow-up" size={12} color="white" />
|
||||||
|
</View>
|
||||||
|
<Text className="text-white/80 text-sm font-medium">Income</Text>
|
||||||
|
</View>
|
||||||
|
<Text className="text-white text-xl font-bold">€{income.toFixed(2)}</Text>
|
||||||
|
</View>
|
||||||
|
<View className="flex-1 bg-red-500 p-4 rounded-2xl shadow-sm">
|
||||||
|
<View className="flex-row items-center mb-2">
|
||||||
|
<View className="bg-white/20 w-8 h-8 rounded-full items-center justify-center mr-2">
|
||||||
|
<FontAwesome name="arrow-down" size={12} color="white" />
|
||||||
|
</View>
|
||||||
|
<Text className="text-white/80 text-sm font-medium">Expenses</Text>
|
||||||
|
</View>
|
||||||
|
<Text className="text-white text-xl font-bold">€{expense.toFixed(2)}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Spending Chart */}
|
||||||
|
{chartData.length > 0 && (
|
||||||
|
<View className="bg-white dark:bg-gray-800 p-4 rounded-2xl shadow-sm mb-6">
|
||||||
|
<Text className="text-lg font-bold text-gray-900 dark:text-white mb-4">Expenses by Category</Text>
|
||||||
|
<BarChart
|
||||||
|
data={chartData}
|
||||||
|
barWidth={30}
|
||||||
|
noOfSections={3}
|
||||||
|
barBorderRadius={4}
|
||||||
|
frontColor="#EF4444"
|
||||||
|
yAxisThickness={0}
|
||||||
|
xAxisThickness={0}
|
||||||
|
hideRules
|
||||||
|
isAnimated
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Recent Transactions */}
|
||||||
|
<View className="mb-4 flex-row justify-between items-end">
|
||||||
|
<Text className="text-xl font-bold text-gray-900 dark:text-white">Recent Transactions</Text>
|
||||||
|
<TouchableOpacity>
|
||||||
|
<Text className="text-blue-600 font-medium">See All</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<TransactionList transactions={transactions} />
|
||||||
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
},
|
|
||||||
separator: {
|
|
||||||
marginVertical: 30,
|
|
||||||
height: 1,
|
|
||||||
width: '80%',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
import { FontAwesome } from '@expo/vector-icons';
|
||||||
|
import { useFocusEffect } from 'expo-router';
|
||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import { ScrollView, Text, View } from 'react-native';
|
||||||
|
import { PieChart } from 'react-native-gifted-charts';
|
||||||
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
import { getDB } from '../../db';
|
||||||
|
import { Asset } from '../../types';
|
||||||
|
|
||||||
|
export default function InvestmentsScreen() {
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
const [assets, setAssets] = useState<Asset[]>([]);
|
||||||
|
const [totalValue, setTotalValue] = useState(0);
|
||||||
|
const [pieData, setPieData] = useState<any[]>([]);
|
||||||
|
|
||||||
|
const fetchData = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const db = getDB();
|
||||||
|
const result = await db.getAllAsync<Asset>('SELECT * FROM assets ORDER BY value DESC');
|
||||||
|
setAssets(result);
|
||||||
|
|
||||||
|
const total = result.reduce((acc, asset) => acc + asset.value, 0);
|
||||||
|
setTotalValue(total);
|
||||||
|
|
||||||
|
// Prepare Pie Data
|
||||||
|
const colors = ['#9333EA', '#C084FC', '#A855F7', '#7E22CE', '#6B21A8'];
|
||||||
|
const data = result.map((asset, index) => ({
|
||||||
|
value: asset.value,
|
||||||
|
color: colors[index % colors.length],
|
||||||
|
text: `${Math.round((asset.value / total) * 100)}%`,
|
||||||
|
}));
|
||||||
|
setPieData(data);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching assets', error);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
fetchData();
|
||||||
|
}, [fetchData])
|
||||||
|
);
|
||||||
|
|
||||||
|
const getIconName = (type: string) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'stock': return 'line-chart';
|
||||||
|
case 'crypto': return 'bitcoin';
|
||||||
|
case 'real_estate': return 'home';
|
||||||
|
case 'fund': return 'pie-chart';
|
||||||
|
default: return 'money';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="flex-1 bg-gray-100 dark:bg-gray-900" style={{ paddingTop: insets.top }}>
|
||||||
|
<ScrollView className="flex-1 px-6">
|
||||||
|
{/* Header */}
|
||||||
|
<View className="py-6">
|
||||||
|
<Text className="text-gray-500 text-sm">Total Portfolio Value</Text>
|
||||||
|
<Text className="text-4xl font-bold text-gray-900 dark:text-white">€{totalValue.toFixed(2)}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Asset Distribution */}
|
||||||
|
<View className="bg-white dark:bg-gray-800 rounded-2xl p-6 mb-8 shadow-sm items-center">
|
||||||
|
<Text className="text-lg font-bold text-gray-900 dark:text-white mb-4 self-start">Asset Allocation</Text>
|
||||||
|
{pieData.length > 0 ? (
|
||||||
|
<PieChart
|
||||||
|
data={pieData}
|
||||||
|
donut
|
||||||
|
showText
|
||||||
|
textColor="white"
|
||||||
|
radius={100}
|
||||||
|
innerRadius={60}
|
||||||
|
textSize={12}
|
||||||
|
focusOnPress
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text className="text-gray-500">No assets to display.</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Assets List */}
|
||||||
|
<Text className="text-xl font-bold text-gray-900 dark:text-white mb-4">Your Assets</Text>
|
||||||
|
|
||||||
|
{assets.length === 0 ? (
|
||||||
|
<View className="items-center py-10">
|
||||||
|
<Text className="text-gray-400">No assets added yet.</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
assets.map((item) => (
|
||||||
|
<View key={item.id} className="bg-white dark:bg-gray-800 p-4 mb-3 rounded-2xl shadow-sm flex-row items-center justify-between">
|
||||||
|
<View className="flex-row items-center">
|
||||||
|
<View className="w-10 h-10 rounded-full bg-purple-100 items-center justify-center mr-4">
|
||||||
|
<FontAwesome name={getIconName(item.type)} size={16} color="#9333EA" />
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<Text className="font-bold text-gray-900 dark:text-white text-base">{item.name}</Text>
|
||||||
|
<Text className="text-gray-500 text-xs capitalize">{item.type.replace('_', ' ')}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View className="items-end">
|
||||||
|
<Text className="font-bold text-base text-gray-900 dark:text-white">€{item.value.toFixed(2)}</Text>
|
||||||
|
{item.quantity && (
|
||||||
|
<Text className="text-gray-400 text-xs">{item.quantity} units</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
import { StyleSheet } from 'react-native';
|
|
||||||
|
|
||||||
import EditScreenInfo from '@/components/EditScreenInfo';
|
|
||||||
import { Text, View } from '@/components/Themed';
|
|
||||||
|
|
||||||
export default function TabTwoScreen() {
|
|
||||||
return (
|
|
||||||
<View style={styles.container}>
|
|
||||||
<Text style={styles.title}>Tab Two</Text>
|
|
||||||
<View style={styles.separator} lightColor="#eee" darkColor="rgba(255,255,255,0.1)" />
|
|
||||||
<EditScreenInfo path="app/(tabs)/two.tsx" />
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
},
|
|
||||||
separator: {
|
|
||||||
marginVertical: 30,
|
|
||||||
height: 1,
|
|
||||||
width: '80%',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -4,13 +4,14 @@ import { useFonts } from 'expo-font';
|
||||||
import { Stack } from 'expo-router';
|
import { Stack } from 'expo-router';
|
||||||
import * as SplashScreen from 'expo-splash-screen';
|
import * as SplashScreen from 'expo-splash-screen';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import 'react-native-reanimated';
|
import { initDatabase } from '../db';
|
||||||
|
import '../global.css';
|
||||||
|
|
||||||
import { useColorScheme } from '@/components/useColorScheme';
|
import { useColorScheme } from '@/components/useColorScheme';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
// Catch any errors thrown by the Layout component.
|
// Catch any errors thrown by the Layout component.
|
||||||
ErrorBoundary,
|
ErrorBoundary
|
||||||
} from 'expo-router';
|
} from 'expo-router';
|
||||||
|
|
||||||
export const unstable_settings = {
|
export const unstable_settings = {
|
||||||
|
|
@ -32,6 +33,12 @@ export default function RootLayout() {
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
}, [error]);
|
}, [error]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initDatabase()
|
||||||
|
.then(() => console.log('Database initialized'))
|
||||||
|
.catch(e => console.error('Database init failed', e));
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loaded) {
|
if (loaded) {
|
||||||
SplashScreen.hideAsync();
|
SplashScreen.hideAsync();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
import { router } from 'expo-router';
|
||||||
|
import { StatusBar } from 'expo-status-bar';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { KeyboardAvoidingView, Platform, ScrollView, Text, TextInput, TouchableOpacity, View } from 'react-native';
|
||||||
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
import { getDB } from '../db';
|
||||||
|
|
||||||
|
export default function AddAssetScreen() {
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [type, setType] = useState('stock');
|
||||||
|
const [value, setValue] = useState('');
|
||||||
|
const [quantity, setQuantity] = useState('');
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!name || !value) {
|
||||||
|
alert('Please fill in name and value');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const db = getDB();
|
||||||
|
await db.runAsync(
|
||||||
|
'INSERT INTO assets (name, type, value, quantity, purchase_date) VALUES (?, ?, ?, ?, ?)',
|
||||||
|
[name, type, parseFloat(value), quantity ? parseFloat(quantity) : null, new Date().toISOString()]
|
||||||
|
);
|
||||||
|
router.back();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
alert('Failed to save asset');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="flex-1 bg-gray-100 dark:bg-gray-900">
|
||||||
|
<StatusBar style={Platform.OS === 'ios' ? 'light' : 'auto'} />
|
||||||
|
|
||||||
|
<View className="bg-purple-600 pt-4 pb-6 px-6 rounded-b-3xl shadow-lg">
|
||||||
|
<Text className="text-white text-2xl font-bold mt-8 text-center">New Asset</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<KeyboardAvoidingView
|
||||||
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
<ScrollView className="flex-1 px-6 pt-6">
|
||||||
|
|
||||||
|
{/* Type Selector */}
|
||||||
|
<ScrollView horizontal showsHorizontalScrollIndicator={false} className="flex-row space-x-2 mb-6">
|
||||||
|
{['stock', 'crypto', 'real_estate', 'fund', 'other'].map((t) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={t}
|
||||||
|
onPress={() => setType(t)}
|
||||||
|
className={`px-4 py-2 rounded-full border ${type === t ? 'bg-purple-600 border-purple-600' : 'bg-white border-gray-200 dark:bg-gray-800 dark:border-gray-700'}`}
|
||||||
|
>
|
||||||
|
<Text className={`capitalize ${type === t ? 'text-white' : 'text-gray-900 dark:text-gray-300'}`}>{t.replace('_', ' ')}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* Value Input */}
|
||||||
|
<View className="bg-white dark:bg-gray-800 rounded-2xl p-6 mb-4 shadow-sm">
|
||||||
|
<Text className="text-gray-500 text-sm mb-2">Current Value</Text>
|
||||||
|
<View className="flex-row items-center">
|
||||||
|
<Text className="text-3xl font-bold text-gray-900 dark:text-white mr-2">€</Text>
|
||||||
|
<TextInput
|
||||||
|
className="flex-1 text-4xl font-bold text-gray-900 dark:text-white"
|
||||||
|
placeholder="0.00"
|
||||||
|
keyboardType="numeric"
|
||||||
|
value={value}
|
||||||
|
onChangeText={setValue}
|
||||||
|
placeholderTextColor="#9CA3AF"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Details Form */}
|
||||||
|
<View className="bg-white dark:bg-gray-800 rounded-2xl p-6 shadow-sm space-y-4">
|
||||||
|
<View>
|
||||||
|
<Text className="text-gray-500 text-sm mb-2">Asset Name</Text>
|
||||||
|
<TextInput
|
||||||
|
className="w-full bg-gray-50 dark:bg-gray-700 p-4 rounded-xl text-gray-900 dark:text-white"
|
||||||
|
placeholder="e.g. Apple Stock, Bitcoin"
|
||||||
|
value={name}
|
||||||
|
onChangeText={setName}
|
||||||
|
placeholderTextColor="#9CA3AF"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="mt-4">
|
||||||
|
<Text className="text-gray-500 text-sm mb-2">Quantity (Optional)</Text>
|
||||||
|
<TextInput
|
||||||
|
className="w-full bg-gray-50 dark:bg-gray-700 p-4 rounded-xl text-gray-900 dark:text-white"
|
||||||
|
placeholder="0"
|
||||||
|
keyboardType="numeric"
|
||||||
|
value={quantity}
|
||||||
|
onChangeText={setQuantity}
|
||||||
|
placeholderTextColor="#9CA3AF"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<View className="p-6 bg-white dark:bg-gray-900 border-t border-gray-100 dark:border-gray-800" style={{ paddingBottom: Math.max(insets.bottom, 24) }}>
|
||||||
|
<TouchableOpacity
|
||||||
|
className="w-full bg-purple-600 py-4 rounded-2xl shadow-lg active:bg-purple-700"
|
||||||
|
onPress={handleSave}
|
||||||
|
>
|
||||||
|
<Text className="text-white text-center font-bold text-lg">Save Asset</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
import { router } from 'expo-router';
|
||||||
|
import { StatusBar } from 'expo-status-bar';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { KeyboardAvoidingView, Platform, ScrollView, Text, TextInput, TouchableOpacity, View } from 'react-native';
|
||||||
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
import { getDB } from '../db';
|
||||||
|
|
||||||
|
export default function AddGoalScreen() {
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [targetAmount, setTargetAmount] = useState('');
|
||||||
|
const [currentAmount, setCurrentAmount] = useState('');
|
||||||
|
const [deadline, setDeadline] = useState('');
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!name || !targetAmount) {
|
||||||
|
alert('Please fill in name and target amount');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const db = getDB();
|
||||||
|
await db.runAsync(
|
||||||
|
'INSERT INTO goals (name, target_amount, current_amount, deadline) VALUES (?, ?, ?, ?)',
|
||||||
|
[name, parseFloat(targetAmount), currentAmount ? parseFloat(currentAmount) : 0, deadline]
|
||||||
|
);
|
||||||
|
router.back();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
alert('Failed to save goal');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="flex-1 bg-gray-100 dark:bg-gray-900">
|
||||||
|
<StatusBar style={Platform.OS === 'ios' ? 'light' : 'auto'} />
|
||||||
|
|
||||||
|
<View className="bg-teal-600 pt-4 pb-6 px-6 rounded-b-3xl shadow-lg">
|
||||||
|
<Text className="text-white text-2xl font-bold mt-8 text-center">New Goal</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<KeyboardAvoidingView
|
||||||
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
<ScrollView className="flex-1 px-6 pt-6">
|
||||||
|
|
||||||
|
{/* Target Amount Input */}
|
||||||
|
<View className="bg-white dark:bg-gray-800 rounded-2xl p-6 mb-4 shadow-sm">
|
||||||
|
<Text className="text-gray-500 text-sm mb-2">Target Amount</Text>
|
||||||
|
<View className="flex-row items-center">
|
||||||
|
<Text className="text-3xl font-bold text-gray-900 dark:text-white mr-2">€</Text>
|
||||||
|
<TextInput
|
||||||
|
className="flex-1 text-4xl font-bold text-gray-900 dark:text-white"
|
||||||
|
placeholder="1000.00"
|
||||||
|
keyboardType="numeric"
|
||||||
|
value={targetAmount}
|
||||||
|
onChangeText={setTargetAmount}
|
||||||
|
placeholderTextColor="#9CA3AF"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Details Form */}
|
||||||
|
<View className="bg-white dark:bg-gray-800 rounded-2xl p-6 shadow-sm space-y-4">
|
||||||
|
<View>
|
||||||
|
<Text className="text-gray-500 text-sm mb-2">Goal Name</Text>
|
||||||
|
<TextInput
|
||||||
|
className="w-full bg-gray-50 dark:bg-gray-700 p-4 rounded-xl text-gray-900 dark:text-white"
|
||||||
|
placeholder="e.g. New Car, Vacation"
|
||||||
|
value={name}
|
||||||
|
onChangeText={setName}
|
||||||
|
placeholderTextColor="#9CA3AF"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="mt-4">
|
||||||
|
<Text className="text-gray-500 text-sm mb-2">Current Savings (Optional)</Text>
|
||||||
|
<TextInput
|
||||||
|
className="w-full bg-gray-50 dark:bg-gray-700 p-4 rounded-xl text-gray-900 dark:text-white"
|
||||||
|
placeholder="0.00"
|
||||||
|
keyboardType="numeric"
|
||||||
|
value={currentAmount}
|
||||||
|
onChangeText={setCurrentAmount}
|
||||||
|
placeholderTextColor="#9CA3AF"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="mt-4">
|
||||||
|
<Text className="text-gray-500 text-sm mb-2">Deadline (Optional)</Text>
|
||||||
|
<TextInput
|
||||||
|
className="w-full bg-gray-50 dark:bg-gray-700 p-4 rounded-xl text-gray-900 dark:text-white"
|
||||||
|
placeholder="YYYY-MM-DD"
|
||||||
|
value={deadline}
|
||||||
|
onChangeText={setDeadline}
|
||||||
|
placeholderTextColor="#9CA3AF"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<View className="p-6 bg-white dark:bg-gray-900 border-t border-gray-100 dark:border-gray-800" style={{ paddingBottom: Math.max(insets.bottom, 24) }}>
|
||||||
|
<TouchableOpacity
|
||||||
|
className="w-full bg-teal-600 py-4 rounded-2xl shadow-lg active:bg-teal-700"
|
||||||
|
onPress={handleSave}
|
||||||
|
>
|
||||||
|
<Text className="text-white text-center font-bold text-lg">Save Goal</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
162
app/modal.tsx
162
app/modal.tsx
|
|
@ -1,35 +1,143 @@
|
||||||
|
import { router } from 'expo-router';
|
||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
import { Platform, StyleSheet } from 'react-native';
|
import { useState } from 'react';
|
||||||
|
import { KeyboardAvoidingView, Platform, ScrollView, Text, TextInput, TouchableOpacity, View } from 'react-native';
|
||||||
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
import { getDB } from '../db';
|
||||||
|
|
||||||
import EditScreenInfo from '@/components/EditScreenInfo';
|
export default function AddTransactionScreen() {
|
||||||
import { Text, View } from '@/components/Themed';
|
const insets = useSafeAreaInsets();
|
||||||
|
const [amount, setAmount] = useState('');
|
||||||
|
const [category, setCategory] = useState('');
|
||||||
|
const [description, setDescription] = useState('');
|
||||||
|
const [type, setType] = useState<'income' | 'expense'>('expense');
|
||||||
|
const [date, setDate] = useState(new Date().toISOString().split('T')[0]); // YYYY-MM-DD
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!amount || !category) {
|
||||||
|
alert('Please fill in amount and category');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const db = getDB();
|
||||||
|
await db.runAsync(
|
||||||
|
'INSERT INTO transactions (amount, category, date, description, type) VALUES (?, ?, ?, ?, ?)',
|
||||||
|
[parseFloat(amount), category, date, description, type]
|
||||||
|
);
|
||||||
|
router.back();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
alert('Failed to save transaction');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default function ModalScreen() {
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View className="flex-1 bg-gray-100 dark:bg-gray-900">
|
||||||
<Text style={styles.title}>Modal</Text>
|
|
||||||
<View style={styles.separator} lightColor="#eee" darkColor="rgba(255,255,255,0.1)" />
|
|
||||||
<EditScreenInfo path="app/modal.tsx" />
|
|
||||||
|
|
||||||
{/* Use a light status bar on iOS to account for the black space above the modal */}
|
|
||||||
<StatusBar style={Platform.OS === 'ios' ? 'light' : 'auto'} />
|
<StatusBar style={Platform.OS === 'ios' ? 'light' : 'auto'} />
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<View className="bg-blue-600 pt-4 pb-6 px-6 rounded-b-3xl shadow-lg">
|
||||||
|
<Text className="text-white text-2xl font-bold mt-8 text-center">New Transaction</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<KeyboardAvoidingView
|
||||||
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
<ScrollView className="flex-1 px-6 pt-6">
|
||||||
|
|
||||||
|
{/* Type Selector */}
|
||||||
|
<View className="flex-row bg-white dark:bg-gray-800 rounded-xl p-1 mb-6 shadow-sm">
|
||||||
|
<TouchableOpacity
|
||||||
|
className={`flex-1 py-3 rounded-lg ${type === 'expense' ? 'bg-red-500' : 'bg-transparent'}`}
|
||||||
|
onPress={() => setType('expense')}
|
||||||
|
>
|
||||||
|
<Text className={`text-center font-semibold ${type === 'expense' ? 'text-white' : 'text-gray-500'}`}>Expense</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
className={`flex-1 py-3 rounded-lg ${type === 'income' ? 'bg-green-500' : 'bg-transparent'}`}
|
||||||
|
onPress={() => setType('income')}
|
||||||
|
>
|
||||||
|
<Text className={`text-center font-semibold ${type === 'income' ? 'text-white' : 'text-gray-500'}`}>Income</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Amount Input */}
|
||||||
|
<View className="bg-white dark:bg-gray-800 rounded-2xl p-6 mb-4 shadow-sm">
|
||||||
|
<Text className="text-gray-500 text-sm mb-2">Amount</Text>
|
||||||
|
<View className="flex-row items-center">
|
||||||
|
<Text className="text-3xl font-bold text-gray-900 dark:text-white mr-2">€</Text>
|
||||||
|
<TextInput
|
||||||
|
className="flex-1 text-4xl font-bold text-gray-900 dark:text-white"
|
||||||
|
placeholder="0.00"
|
||||||
|
keyboardType="numeric"
|
||||||
|
value={amount}
|
||||||
|
onChangeText={setAmount}
|
||||||
|
placeholderTextColor="#9CA3AF"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Details Form */}
|
||||||
|
<View className="bg-white dark:bg-gray-800 rounded-2xl p-6 shadow-sm space-y-4">
|
||||||
|
<View>
|
||||||
|
<Text className="text-gray-500 text-sm mb-2">Category</Text>
|
||||||
|
<ScrollView horizontal showsHorizontalScrollIndicator={false} className="flex-row space-x-2 mb-2">
|
||||||
|
{['Food', 'Transport', 'Shopping', 'Entertainment', 'Bills', 'Health', 'Salary', 'Investment', 'Other'].map((cat) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={cat}
|
||||||
|
onPress={() => setCategory(cat)}
|
||||||
|
className={`px-4 py-2 rounded-full border ${category === cat ? 'bg-blue-600 border-blue-600' : 'bg-gray-50 border-gray-200 dark:bg-gray-700 dark:border-gray-600'}`}
|
||||||
|
>
|
||||||
|
<Text className={`${category === cat ? 'text-white' : 'text-gray-900 dark:text-gray-300'}`}>{cat}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
<TextInput
|
||||||
|
className="w-full bg-gray-50 dark:bg-gray-700 p-4 rounded-xl text-gray-900 dark:text-white"
|
||||||
|
placeholder="Or type custom category..."
|
||||||
|
value={category}
|
||||||
|
onChangeText={setCategory}
|
||||||
|
placeholderTextColor="#9CA3AF"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="mt-4">
|
||||||
|
<Text className="text-gray-500 text-sm mb-2">Date</Text>
|
||||||
|
<TextInput
|
||||||
|
className="w-full bg-gray-50 dark:bg-gray-700 p-4 rounded-xl text-gray-900 dark:text-white"
|
||||||
|
placeholder="YYYY-MM-DD"
|
||||||
|
value={date}
|
||||||
|
onChangeText={setDate}
|
||||||
|
placeholderTextColor="#9CA3AF"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="mt-4">
|
||||||
|
<Text className="text-gray-500 text-sm mb-2">Description (Optional)</Text>
|
||||||
|
<TextInput
|
||||||
|
className="w-full bg-gray-50 dark:bg-gray-700 p-4 rounded-xl text-gray-900 dark:text-white"
|
||||||
|
placeholder="Add a note..."
|
||||||
|
value={description}
|
||||||
|
onChangeText={setDescription}
|
||||||
|
placeholderTextColor="#9CA3AF"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* Save Button */}
|
||||||
|
<View className="p-6 bg-white dark:bg-gray-900 border-t border-gray-100 dark:border-gray-800" style={{ paddingBottom: Math.max(insets.bottom, 24) }}>
|
||||||
|
<TouchableOpacity
|
||||||
|
className="w-full bg-blue-600 py-4 rounded-2xl shadow-lg active:bg-blue-700"
|
||||||
|
onPress={handleSave}
|
||||||
|
>
|
||||||
|
<Text className="text-white text-center font-bold text-lg">Save Transaction</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
},
|
|
||||||
separator: {
|
|
||||||
marginVertical: 30,
|
|
||||||
height: 1,
|
|
||||||
width: '80%',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
module.exports = function (api) {
|
||||||
|
api.cache(true);
|
||||||
|
return {
|
||||||
|
presets: [
|
||||||
|
["babel-preset-expo", { jsxImportSource: "nativewind" }],
|
||||||
|
],
|
||||||
|
plugins: ["react-native-reanimated/plugin"],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { FontAwesome } from '@expo/vector-icons';
|
||||||
|
import { FlatList, Text, View } from 'react-native';
|
||||||
|
import { Transaction } from '../types';
|
||||||
|
|
||||||
|
interface TransactionListProps {
|
||||||
|
transactions: Transaction[];
|
||||||
|
onDelete?: (id: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TransactionList({ transactions, onDelete }: TransactionListProps) {
|
||||||
|
if (transactions.length === 0) {
|
||||||
|
return (
|
||||||
|
<View className="flex-1 justify-center items-center py-10">
|
||||||
|
<Text className="text-gray-400 text-lg">No transactions yet</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FlatList
|
||||||
|
data={transactions}
|
||||||
|
keyExtractor={(item) => item.id.toString()}
|
||||||
|
contentContainerStyle={{ paddingBottom: 20 }}
|
||||||
|
renderItem={({ item }) => (
|
||||||
|
<View className="bg-white dark:bg-gray-800 p-4 mb-3 rounded-2xl shadow-sm flex-row items-center justify-between">
|
||||||
|
<View className="flex-row items-center">
|
||||||
|
<View className={`w-10 h-10 rounded-full items-center justify-center mr-4 ${item.type === 'expense' ? 'bg-red-100' : 'bg-green-100'}`}>
|
||||||
|
<FontAwesome
|
||||||
|
name={item.type === 'expense' ? 'arrow-down' : 'arrow-up'}
|
||||||
|
size={16}
|
||||||
|
color={item.type === 'expense' ? '#EF4444' : '#10B981'}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<Text className="font-bold text-gray-900 dark:text-white text-base">{item.category}</Text>
|
||||||
|
<Text className="text-gray-500 text-xs">{item.date}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View className="items-end">
|
||||||
|
<Text className={`font-bold text-base ${item.type === 'expense' ? 'text-red-500' : 'text-green-500'}`}>
|
||||||
|
{item.type === 'expense' ? '-' : '+'}€{item.amount.toFixed(2)}
|
||||||
|
</Text>
|
||||||
|
{item.description && (
|
||||||
|
<Text className="text-gray-400 text-xs max-w-[100px]" numberOfLines={1}>{item.description}</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { openDatabaseSync } from 'expo-sqlite';
|
||||||
|
import { Platform } from 'react-native';
|
||||||
|
|
||||||
|
let db: any;
|
||||||
|
if (Platform.OS !== 'web') {
|
||||||
|
db = openDatabaseSync('finance.db');
|
||||||
|
} else {
|
||||||
|
// Mock DB for web to prevent crash
|
||||||
|
db = {
|
||||||
|
execSync: () => { },
|
||||||
|
getAllAsync: async () => [],
|
||||||
|
runAsync: async () => { },
|
||||||
|
getFirstAsync: async () => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initDatabase = () => {
|
||||||
|
try {
|
||||||
|
db.execSync(`
|
||||||
|
CREATE TABLE IF NOT EXISTS transactions (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
amount REAL NOT NULL,
|
||||||
|
category TEXT NOT NULL,
|
||||||
|
date TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
type TEXT NOT NULL -- 'income' or 'expense'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS assets (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
type TEXT NOT NULL, -- 'stock', 'crypto', 'real_estate', etc.
|
||||||
|
value REAL NOT NULL,
|
||||||
|
quantity REAL,
|
||||||
|
purchase_date TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS goals (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
target_amount REAL NOT NULL,
|
||||||
|
current_amount REAL NOT NULL DEFAULT 0,
|
||||||
|
deadline TEXT
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
console.log('Database initialized successfully');
|
||||||
|
return Promise.resolve(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error initializing database', error);
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDB = () => db;
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
const { getDefaultConfig } = require("expo/metro-config");
|
||||||
|
const { withNativeWind } = require("nativewind/metro");
|
||||||
|
|
||||||
|
const config = getDefaultConfig(__dirname);
|
||||||
|
|
||||||
|
config.resolver.assetExts.push("wasm");
|
||||||
|
|
||||||
|
module.exports = withNativeWind(config, { input: "./global.css" });
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
/// <reference types="nativewind/types" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited and should be committed with your source code. It is generated by NativeWind.
|
||||||
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
|
|
@ -11,22 +11,30 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/vector-icons": "^15.0.3",
|
"@expo/vector-icons": "^15.0.3",
|
||||||
"@react-navigation/native": "^7.1.8",
|
"@react-navigation/native": "^7.1.8",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
"expo": "~54.0.25",
|
"expo": "~54.0.25",
|
||||||
"expo-constants": "~18.0.10",
|
"expo-constants": "~18.0.10",
|
||||||
"expo-font": "~14.0.9",
|
"expo-font": "~14.0.9",
|
||||||
|
"expo-linear-gradient": "~15.0.7",
|
||||||
"expo-linking": "~8.0.9",
|
"expo-linking": "~8.0.9",
|
||||||
"expo-router": "~6.0.15",
|
"expo-router": "~6.0.15",
|
||||||
"expo-splash-screen": "~31.0.11",
|
"expo-splash-screen": "~31.0.11",
|
||||||
|
"expo-sqlite": "^16.0.9",
|
||||||
"expo-status-bar": "~3.0.8",
|
"expo-status-bar": "~3.0.8",
|
||||||
"expo-web-browser": "~15.0.9",
|
"expo-web-browser": "~15.0.9",
|
||||||
|
"nativewind": "^4.2.1",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-native": "0.81.5",
|
"react-native": "0.81.5",
|
||||||
"react-native-worklets": "0.5.1",
|
"react-native-gifted-charts": "^1.4.66",
|
||||||
"react-native-reanimated": "~4.1.1",
|
"react-native-reanimated": "~4.1.1",
|
||||||
"react-native-safe-area-context": "~5.6.0",
|
"react-native-safe-area-context": "~5.6.0",
|
||||||
"react-native-screens": "~4.16.0",
|
"react-native-screens": "~4.16.0",
|
||||||
"react-native-web": "~0.21.0"
|
"react-native-svg": "^15.12.1",
|
||||||
|
"react-native-web": "~0.21.0",
|
||||||
|
"react-native-worklets": "0.5.1",
|
||||||
|
"tailwind-merge": "^3.4.0",
|
||||||
|
"tailwindcss": "^3.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "~19.1.0",
|
"@types/react": "~19.1.0",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: ["./app/**/*.{js,jsx,ts,tsx}", "./components/**/*.{js,jsx,ts,tsx}"],
|
||||||
|
presets: [require("nativewind/preset")],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
"**/*.ts",
|
"**/*.ts",
|
||||||
"**/*.tsx",
|
"**/*.tsx",
|
||||||
".expo/types/**/*.ts",
|
".expo/types/**/*.ts",
|
||||||
"expo-env.d.ts"
|
"expo-env.d.ts",
|
||||||
|
"nativewind-env.d.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
export interface Transaction {
|
||||||
|
id: number;
|
||||||
|
amount: number;
|
||||||
|
category: string;
|
||||||
|
date: string; // ISO 8601 YYYY-MM-DD
|
||||||
|
description?: string;
|
||||||
|
type: 'income' | 'expense';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Asset {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
type: 'stock' | 'crypto' | 'real_estate' | 'fund' | 'other';
|
||||||
|
value: number;
|
||||||
|
quantity?: number;
|
||||||
|
purchase_date?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Goal {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
target_amount: number;
|
||||||
|
current_amount: number;
|
||||||
|
deadline?: string;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue