Kantongku: aplikasi pencatat keuangan pribadi offline-first

This commit is contained in:
Bro
2026-08-16 12:22:36 +07:00
commit e0c4cef96b
57 changed files with 11641 additions and 0 deletions
+208
View File
@@ -0,0 +1,208 @@
import { Ionicons } from '@expo/vector-icons';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { Pressable, StyleSheet, View } from 'react-native';
import { colors } from '../theme/colors';
import { DashboardScreen } from '../screens/DashboardScreen';
import { HistoryScreen } from '../screens/HistoryScreen';
import { AddTransactionScreen } from '../screens/AddTransactionScreen';
import { EditTransactionScreen } from '../screens/EditTransactionScreen';
import { RecurringFormScreen } from '../screens/RecurringFormScreen';
import { TargetFormScreen } from '../screens/TargetFormScreen';
import { SavingsScreen } from '../screens/SavingsScreen';
import { SettingsScreen } from '../screens/SettingsScreen';
import type { TransactionType } from '../types';
export type RootTabParamList = {
Beranda: undefined;
Riwayat: undefined;
Catat: { type?: TransactionType } | undefined;
Tabungan: undefined;
Pengaturan: undefined;
};
export type RootStackParamList = {
Main: undefined;
EditTransaksi: { transactionId: number };
RecurringForm: undefined;
TargetForm: undefined;
};
const Tab = createBottomTabNavigator<RootTabParamList>();
const Stack = createNativeStackNavigator<RootStackParamList>();
type IoniconName = React.ComponentProps<typeof Ionicons>['name'];
const tabIcon =
(focusedName: IoniconName, unfocusedName: IoniconName) =>
({ color, size, focused }: { color: string; size: number; focused: boolean }) =>
(
<Ionicons
name={focused ? focusedName : unfocusedName}
size={size}
color={color}
/>
);
/** Tombol "+" melingkar di tengah tab bar (FAB) */
function FabButton({
onPress,
accessibilityState,
}: {
onPress?: (...args: any[]) => void;
accessibilityState?: { selected?: boolean };
}) {
const focused = accessibilityState?.selected;
return (
<Pressable
onPress={onPress}
style={({ pressed }) => [styles.fabWrapper, pressed && styles.fabPressed]}
hitSlop={8}
>
<View style={[styles.fab, focused && styles.fabActive]}>
<Ionicons name="add" size={32} color={colors.white} />
</View>
</Pressable>
);
}
function MainTabs() {
return (
<Tab.Navigator
screenOptions={{
tabBarActiveTintColor: colors.primary,
tabBarInactiveTintColor: colors.textMuted,
tabBarLabelStyle: styles.tabLabel,
tabBarStyle: styles.tabBar,
headerTitleStyle: { fontWeight: '700', color: colors.text },
headerStyle: { backgroundColor: colors.background },
headerShadowVisible: false,
}}
>
<Tab.Screen
name="Beranda"
component={DashboardScreen}
options={{
headerShown: false,
tabBarIcon: tabIcon('home', 'home-outline'),
}}
/>
<Tab.Screen
name="Riwayat"
component={HistoryScreen}
options={{
title: 'Riwayat',
tabBarIcon: tabIcon('list', 'list-outline'),
}}
/>
<Tab.Screen
name="Catat"
component={AddTransactionScreen}
options={{
title: 'Catat',
tabBarIcon: () => null,
tabBarLabel: () => null,
tabBarButton: (props) => <FabButton {...props} />,
}}
/>
<Tab.Screen
name="Tabungan"
component={SavingsScreen}
options={{
tabBarIcon: tabIcon('wallet', 'wallet-outline'),
}}
/>
<Tab.Screen
name="Pengaturan"
component={SettingsScreen}
options={{
tabBarIcon: tabIcon('settings', 'settings-outline'),
}}
/>
</Tab.Navigator>
);
}
export function RootNavigator() {
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="Main" component={MainTabs} />
<Stack.Screen
name="EditTransaksi"
component={EditTransactionScreen}
options={{
presentation: 'modal',
headerShown: true,
title: 'Edit Transaksi',
headerTitleStyle: { fontWeight: '700', color: colors.text },
headerStyle: { backgroundColor: colors.background },
headerShadowVisible: false,
}}
/>
<Stack.Screen
name="RecurringForm"
component={RecurringFormScreen}
options={{
presentation: 'modal',
headerShown: true,
title: 'Transaksi Berulang',
headerTitleStyle: { fontWeight: '700', color: colors.text },
headerStyle: { backgroundColor: colors.background },
headerShadowVisible: false,
}}
/>
<Stack.Screen
name="TargetForm"
component={TargetFormScreen}
options={{
presentation: 'modal',
headerShown: true,
title: 'Target Tabungan',
headerTitleStyle: { fontWeight: '700', color: colors.text },
headerStyle: { backgroundColor: colors.background },
headerShadowVisible: false,
}}
/>
</Stack.Navigator>
);
}
const styles = StyleSheet.create({
tabBar: {
backgroundColor: colors.surface,
borderTopColor: colors.border,
borderTopWidth: 1,
height: 62,
paddingTop: 4,
paddingBottom: 6,
},
tabLabel: {
fontSize: 10,
fontWeight: '600',
},
fabWrapper: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
top: -24,
},
fabPressed: { opacity: 0.85 },
fab: {
width: 58,
height: 58,
borderRadius: 29,
backgroundColor: colors.primary,
alignItems: 'center',
justifyContent: 'center',
borderWidth: 4,
borderColor: colors.surface,
shadowColor: colors.primaryDark,
shadowOpacity: 0.35,
shadowRadius: 10,
shadowOffset: { width: 0, height: 5 },
elevation: 8,
},
fabActive: {
backgroundColor: colors.primaryDark,
},
});