Kantongku: kelola kategori custom (onboarding + pengaturan)

This commit is contained in:
Bro
2026-08-16 12:29:08 +07:00
parent 664d903c05
commit 6c9276c7fa
6 changed files with 701 additions and 4 deletions
+5 -2
View File
@@ -4,13 +4,14 @@ import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
import { SafeAreaProvider } from 'react-native-safe-area-context'; import { SafeAreaProvider } from 'react-native-safe-area-context';
import { NavigationContainer } from '@react-navigation/native'; import { NavigationContainer } from '@react-navigation/native';
import { initDatabase } from './src/db/database'; import { initDatabase } from './src/db/database';
import { checkRecurringDue } from './src/db/repositories'; import { checkRecurringDue, getSetting } from './src/db/repositories';
import { RootNavigator } from './src/navigation'; import { RootNavigator } from './src/navigation';
import { colors } from './src/theme/colors'; import { colors } from './src/theme/colors';
export default function App() { export default function App() {
const [ready, setReady] = useState(false); const [ready, setReady] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [initialRoute, setInitialRoute] = useState<'Main' | 'Onboarding'>('Onboarding');
useEffect(() => { useEffect(() => {
initDatabase() initDatabase()
@@ -21,6 +22,8 @@ export default function App() {
} catch { } catch {
// jangan sampai mengganggu startup // jangan sampai mengganggu startup
} }
const onboarded = await getSetting('onboarded');
setInitialRoute(onboarded === '1' ? 'Main' : 'Onboarding');
setReady(true); setReady(true);
}) })
.catch((e) => setError(String(e))); .catch((e) => setError(String(e)));
@@ -47,7 +50,7 @@ export default function App() {
return ( return (
<SafeAreaProvider> <SafeAreaProvider>
<NavigationContainer> <NavigationContainer>
<RootNavigator /> <RootNavigator initialRoute={initialRoute} />
</NavigationContainer> </NavigationContainer>
<StatusBar style="dark" /> <StatusBar style="dark" />
</SafeAreaProvider> </SafeAreaProvider>
+30
View File
@@ -39,6 +39,36 @@ export async function setSetting(key: string, value: string): Promise<void> {
// ---------- Kategori ---------- // ---------- Kategori ----------
export async function addCategory(input: {
name: string;
type: TransactionType;
icon: string;
}): Promise<number> {
const db = getDb();
const result = await db.runAsync(
'INSERT INTO categories (name, type, icon, is_default) VALUES (?, ?, ?, 0)',
[input.name, input.type, input.icon]
);
return result.lastInsertRowId;
}
export async function updateCategory(
id: number,
input: { name: string; icon: string }
): Promise<void> {
const db = getDb();
await db.runAsync('UPDATE categories SET name = ?, icon = ? WHERE id = ?', [
input.name,
input.icon,
id,
]);
}
export async function deleteCategory(id: number): Promise<void> {
const db = getDb();
await db.runAsync('DELETE FROM categories WHERE id = ?', [id]);
}
export async function getCategories(type?: TransactionType): Promise<Category[]> { export async function getCategories(type?: TransactionType): Promise<Category[]> {
const db = getDb(); const db = getDb();
if (type) { if (type) {
+33 -2
View File
@@ -9,6 +9,8 @@ import { AddTransactionScreen } from '../screens/AddTransactionScreen';
import { EditTransactionScreen } from '../screens/EditTransactionScreen'; import { EditTransactionScreen } from '../screens/EditTransactionScreen';
import { RecurringFormScreen } from '../screens/RecurringFormScreen'; import { RecurringFormScreen } from '../screens/RecurringFormScreen';
import { TargetFormScreen } from '../screens/TargetFormScreen'; import { TargetFormScreen } from '../screens/TargetFormScreen';
import { CategoryFormScreen } from '../screens/CategoryFormScreen';
import { OnboardingScreen } from '../screens/OnboardingScreen';
import { SavingsScreen } from '../screens/SavingsScreen'; import { SavingsScreen } from '../screens/SavingsScreen';
import { SettingsScreen } from '../screens/SettingsScreen'; import { SettingsScreen } from '../screens/SettingsScreen';
import type { TransactionType } from '../types'; import type { TransactionType } from '../types';
@@ -23,9 +25,18 @@ export type RootTabParamList = {
export type RootStackParamList = { export type RootStackParamList = {
Main: undefined; Main: undefined;
Onboarding: undefined;
EditTransaksi: { transactionId: number }; EditTransaksi: { transactionId: number };
RecurringForm: undefined; RecurringForm: undefined;
TargetForm: undefined; TargetForm: undefined;
CategoryForm:
| {
categoryId?: number;
initialName?: string;
initialIcon?: string;
defaultType?: TransactionType;
}
| undefined;
}; };
const Tab = createBottomTabNavigator<RootTabParamList>(); const Tab = createBottomTabNavigator<RootTabParamList>();
@@ -123,10 +134,18 @@ function MainTabs() {
); );
} }
export function RootNavigator() { export function RootNavigator({
initialRoute = 'Main',
}: {
initialRoute?: 'Main' | 'Onboarding';
}) {
return ( return (
<Stack.Navigator screenOptions={{ headerShown: false }}> <Stack.Navigator
initialRouteName={initialRoute}
screenOptions={{ headerShown: false }}
>
<Stack.Screen name="Main" component={MainTabs} /> <Stack.Screen name="Main" component={MainTabs} />
<Stack.Screen name="Onboarding" component={OnboardingScreen} />
<Stack.Screen <Stack.Screen
name="EditTransaksi" name="EditTransaksi"
component={EditTransactionScreen} component={EditTransactionScreen}
@@ -163,6 +182,18 @@ export function RootNavigator() {
headerShadowVisible: false, headerShadowVisible: false,
}} }}
/> />
<Stack.Screen
name="CategoryForm"
component={CategoryFormScreen}
options={({ route }) => ({
presentation: 'modal',
headerShown: true,
title: route.params?.categoryId != null ? 'Edit Kategori' : 'Kategori Baru',
headerTitleStyle: { fontWeight: '700', color: colors.text },
headerStyle: { backgroundColor: colors.background },
headerShadowVisible: false,
})}
/>
</Stack.Navigator> </Stack.Navigator>
); );
} }
+209
View File
@@ -0,0 +1,209 @@
import { useNavigation, useRoute } from '@react-navigation/native';
import type { RouteProp } from '@react-navigation/native';
import { useState } from 'react';
import {
Alert,
KeyboardAvoidingView,
Platform,
Pressable,
ScrollView,
StyleSheet,
Text,
TextInput,
View,
} from 'react-native';
import { colors } from '../theme/colors';
import { addCategory, getCategories, updateCategory } from '../db/repositories';
import type { TransactionType } from '../types';
import type { RootStackParamList } from '../navigation';
const EMOJIS = [
'🍜', '🚌', '🛒', '🧾', '💊', '🎬', '💼', '🎁', '💰', '📦',
'🐱', '🐶', '📱', '💻', '🎮', '🏠', '⚡', '💧', '☕', '🍔',
'🎓', '✈️', '🚗', '⛽', '👕', '💄', '🎉', '📚', '🏋️', '🎵',
'🐷', '💳', '🏦', '📈', '🧧', '🌾', '🔥', '⭐', '🍎', '🧃',
];
export function CategoryFormScreen() {
const navigation = useNavigation();
const route = useRoute<RouteProp<RootStackParamList, 'CategoryForm'>>();
const editing = route.params?.categoryId != null;
const [type, setType] = useState<TransactionType>(route.params?.defaultType ?? 'expense');
const [name, setName] = useState(route.params?.initialName ?? '');
const [icon, setIcon] = useState(route.params?.initialIcon ?? '📦');
const [saving, setSaving] = useState(false);
const handleSave = async () => {
if (!name.trim()) {
Alert.alert('Nama kosong', 'Kasih nama kategorinya dulu, Mas.');
return;
}
setSaving(true);
try {
if (editing && route.params?.categoryId != null) {
await updateCategory(route.params.categoryId, { name: name.trim(), icon });
} else {
await addCategory({ name: name.trim(), type, icon });
}
Alert.alert(editing ? 'Tersimpan ✅' : 'Kategori dibuat ✅', undefined, [
{ text: 'Oke', onPress: () => navigation.goBack() },
]);
} catch (e) {
Alert.alert('Gagal menyimpan', String(e));
} finally {
setSaving(false);
}
};
return (
<KeyboardAvoidingView
style={styles.flex}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
{!editing && (
<>
<Text style={styles.sectionTitle}>Tipe</Text>
<View style={styles.toggleRow}>
<Pressable
style={[styles.toggleBtn, type === 'income' && styles.toggleIncome]}
onPress={() => setType('income')}
>
<Text
style={[
styles.toggleText,
type === 'income' && { color: colors.income, fontWeight: '700' },
]}
>
+ Pemasukan
</Text>
</Pressable>
<Pressable
style={[styles.toggleBtn, type === 'expense' && styles.toggleExpense]}
onPress={() => setType('expense')}
>
<Text
style={[
styles.toggleText,
type === 'expense' && { color: colors.expense, fontWeight: '700' },
]}
>
- Pengeluaran
</Text>
</Pressable>
</View>
</>
)}
<Text style={styles.sectionTitle}>Nama kategori</Text>
<TextInput
style={[styles.card, styles.input]}
value={name}
onChangeText={setName}
placeholder="Misal: Kucing, Gadget, Gaji Sampingan..."
placeholderTextColor={colors.textMuted}
autoFocus={!editing}
/>
<Text style={styles.sectionTitle}>Icon</Text>
<View style={styles.emojiGrid}>
{EMOJIS.map((e) => (
<Pressable
key={e}
style={[styles.emojiChip, icon === e && styles.emojiChipActive]}
onPress={() => setIcon(e)}
>
<Text style={styles.emojiText}>{e}</Text>
</Pressable>
))}
</View>
</ScrollView>
<View style={styles.saveBar}>
<Pressable
onPress={handleSave}
disabled={saving}
style={({ pressed }) => [styles.savePressable, pressed && styles.pressed]}
>
<View style={styles.saveButton}>
<Text style={styles.saveButtonText}>
{saving ? 'Menyimpan...' : editing ? 'Simpan Perubahan' : 'Buat Kategori'}
</Text>
</View>
</Pressable>
</View>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
flex: { flex: 1, backgroundColor: colors.background },
container: { flex: 1 },
content: { padding: 16, paddingBottom: 32 },
sectionTitle: {
fontSize: 14,
fontWeight: '700',
color: colors.text,
marginBottom: 8,
marginTop: 8,
},
toggleRow: {
flexDirection: 'row',
backgroundColor: colors.surface,
borderRadius: 14,
borderWidth: 1,
borderColor: colors.border,
padding: 4,
marginBottom: 8,
},
toggleBtn: { flex: 1, alignItems: 'center', paddingVertical: 10, borderRadius: 10 },
toggleIncome: { backgroundColor: colors.incomeSoft },
toggleExpense: { backgroundColor: colors.expenseSoft },
toggleText: { fontSize: 14, color: colors.textMuted },
card: {
backgroundColor: colors.surface,
borderRadius: 16,
padding: 16,
borderWidth: 1,
borderColor: colors.border,
},
input: { paddingVertical: 12, fontSize: 15, color: colors.text },
emojiGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 },
emojiChip: {
width: 48,
height: 48,
borderRadius: 13,
backgroundColor: colors.surface,
borderWidth: 1,
borderColor: colors.border,
alignItems: 'center',
justifyContent: 'center',
},
emojiChipActive: {
borderColor: colors.primary,
backgroundColor: colors.primarySoft,
},
emojiText: { fontSize: 22 },
saveBar: {
padding: 16,
backgroundColor: colors.background,
borderTopWidth: 1,
borderTopColor: colors.border,
},
savePressable: {
borderRadius: 14,
shadowColor: colors.primaryDark,
shadowOpacity: 0.25,
shadowRadius: 8,
shadowOffset: { width: 0, height: 4 },
elevation: 4,
},
saveButton: {
backgroundColor: colors.primary,
borderRadius: 14,
paddingVertical: 15,
alignItems: 'center',
},
saveButtonText: { color: colors.white, fontWeight: '700', fontSize: 16 },
pressed: { opacity: 0.85 },
});
+342
View File
@@ -0,0 +1,342 @@
import { Ionicons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useState } from 'react';
import {
Alert,
Image,
Pressable,
ScrollView,
StyleSheet,
Text,
TextInput,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { colors } from '../theme/colors';
import {
addCategory,
getCategories,
setSetting,
} from '../db/repositories';
import type { Category, TransactionType } from '../types';
import type { RootStackParamList } from '../navigation';
const QUICK_EMOJIS = ['🍜', '🚌', '🛒', '🧾', '💊', '🎬', '💼', '🎁', '🐱', '📱', '🏠', '🎮'];
export function OnboardingScreen() {
const navigation =
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const [type, setType] = useState<TransactionType>('expense');
const [name, setName] = useState('');
const [icon, setIcon] = useState('🍜');
const [categories, setCategories] = useState<Category[]>([]);
const [loading, setLoading] = useState(true);
// Muat kategori yang sudah ada (default)
useState(() => {
getCategories().then((c) => {
setCategories(c);
setLoading(false);
});
});
const handleAdd = async () => {
if (!name.trim()) {
Alert.alert('Nama kosong', 'Kasih nama kategorinya dulu ya.');
return;
}
await addCategory({ name: name.trim(), type, icon });
setName('');
setCategories(await getCategories());
};
const handleStart = async () => {
await setSetting('onboarded', '1');
navigation.replace('Main');
};
return (
<SafeAreaView style={styles.safe}>
<ScrollView contentContainerStyle={styles.content}>
<LinearGradient
colors={[colors.primary, colors.primaryDark]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.hero}
>
<View style={styles.ornCircleLarge} />
<View style={styles.ornCircleSmall} />
<Image
source={require('../../assets/wallet-3d-teal-logo.png')}
style={styles.logo}
resizeMode="contain"
/>
<Text style={styles.heroTitle}>Selamat datang di Kantongku! 👛</Text>
<Text style={styles.heroSub}>
Catat pemasukan, sisihkan otomatis, dan jaga semangat nabungmu —
semua offline, semua di perangkatmu.
</Text>
</LinearGradient>
<Text style={styles.sectionTitle}>
Tambah kategori favoritmu (opsional)
</Text>
<Text style={styles.sectionHint}>
Misal: Kucing 🐱, Gadget 📱 — biar pencatatanmu makin personal.
</Text>
{/* Toggle tipe */}
<View style={styles.toggleRow}>
<Pressable
style={[styles.toggleBtn, type === 'income' && styles.toggleIncome]}
onPress={() => setType('income')}
>
<Text
style={[
styles.toggleText,
type === 'income' && { color: colors.income, fontWeight: '700' },
]}
>
+ Pemasukan
</Text>
</Pressable>
<Pressable
style={[styles.toggleBtn, type === 'expense' && styles.toggleExpense]}
onPress={() => setType('expense')}
>
<Text
style={[
styles.toggleText,
type === 'expense' && { color: colors.expense, fontWeight: '700' },
]}
>
- Pengeluaran
</Text>
</Pressable>
</View>
<TextInput
style={[styles.card, styles.input]}
value={name}
onChangeText={setName}
placeholder="Nama kategori (misal: Kucing)"
placeholderTextColor={colors.textMuted}
/>
<View style={styles.emojiGrid}>
{QUICK_EMOJIS.map((e) => (
<Pressable
key={e}
style={[styles.emojiChip, icon === e && styles.emojiChipActive]}
onPress={() => setIcon(e)}
>
<Text style={styles.emojiText}>{e}</Text>
</Pressable>
))}
</View>
<Pressable
style={({ pressed }) => [styles.addBtn, pressed && styles.pressed]}
onPress={handleAdd}
>
<Ionicons name="add" size={18} color={colors.white} />
<Text style={styles.addBtnText}>Tambah Kategori</Text>
</Pressable>
{!loading && categories.length > 0 && (
<View style={styles.existingWrap}>
<Text style={styles.existingLabel}>Kategori yang tersedia:</Text>
<View style={styles.chipWrap}>
{categories.map((c) => (
<View key={c.id} style={styles.chip}>
<Text style={styles.chipText}>
{c.icon} {c.name}
</Text>
</View>
))}
</View>
</View>
)}
<Pressable
style={({ pressed }) => [styles.startBtn, pressed && styles.pressed]}
onPress={handleStart}
>
<LinearGradient
colors={[colors.primary, colors.primaryDark]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.startGradient}
>
<Text style={styles.startText}>🚀 Mulai Pakai Kantongku</Text>
</LinearGradient>
</Pressable>
<Text style={styles.footerNote}>
Kategori bisa diubah kapan saja di menu Pengaturan.
</Text>
</ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safe: { flex: 1, backgroundColor: colors.background },
content: { paddingBottom: 32 },
hero: {
marginHorizontal: 16,
marginTop: 16,
borderRadius: 24,
padding: 22,
overflow: 'hidden',
alignItems: 'center',
shadowColor: colors.primaryDark,
shadowOpacity: 0.25,
shadowRadius: 12,
shadowOffset: { width: 0, height: 6 },
elevation: 6,
},
ornCircleLarge: {
position: 'absolute',
right: -40,
top: -50,
width: 160,
height: 160,
borderRadius: 80,
backgroundColor: 'rgba(255,255,255,0.07)',
},
ornCircleSmall: {
position: 'absolute',
left: -30,
bottom: -40,
width: 110,
height: 110,
borderRadius: 55,
backgroundColor: 'rgba(255,255,255,0.06)',
},
logo: { width: 96, height: 96, marginBottom: 12 },
heroTitle: {
fontSize: 22,
fontWeight: '800',
color: colors.white,
textAlign: 'center',
},
heroSub: {
fontSize: 13,
color: 'rgba(255,255,255,0.9)',
textAlign: 'center',
lineHeight: 19,
marginTop: 8,
},
sectionTitle: {
fontSize: 16,
fontWeight: '700',
color: colors.text,
marginTop: 20,
marginHorizontal: 16,
},
sectionHint: {
fontSize: 12,
color: colors.textMuted,
marginHorizontal: 16,
marginTop: 4,
marginBottom: 10,
},
toggleRow: {
flexDirection: 'row',
backgroundColor: colors.surface,
borderRadius: 14,
borderWidth: 1,
borderColor: colors.border,
padding: 4,
marginHorizontal: 16,
marginBottom: 10,
},
toggleBtn: { flex: 1, alignItems: 'center', paddingVertical: 10, borderRadius: 10 },
toggleIncome: { backgroundColor: colors.incomeSoft },
toggleExpense: { backgroundColor: colors.expenseSoft },
toggleText: { fontSize: 14, color: colors.textMuted },
card: {
backgroundColor: colors.surface,
borderRadius: 16,
padding: 16,
borderWidth: 1,
borderColor: colors.border,
},
input: {
marginHorizontal: 16,
paddingVertical: 12,
fontSize: 15,
color: colors.text,
marginBottom: 10,
},
emojiGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
marginHorizontal: 16,
},
emojiChip: {
width: 48,
height: 48,
borderRadius: 13,
backgroundColor: colors.surface,
borderWidth: 1,
borderColor: colors.border,
alignItems: 'center',
justifyContent: 'center',
},
emojiChipActive: {
borderColor: colors.primary,
backgroundColor: colors.primarySoft,
},
emojiText: { fontSize: 22 },
addBtn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 6,
backgroundColor: colors.primary,
borderRadius: 12,
paddingVertical: 12,
marginHorizontal: 16,
marginTop: 12,
},
addBtnText: { color: colors.white, fontWeight: '700', fontSize: 14 },
existingWrap: { marginHorizontal: 16, marginTop: 16 },
existingLabel: { fontSize: 12, color: colors.textMuted, marginBottom: 8 },
chipWrap: { flexDirection: 'row', flexWrap: 'wrap', gap: 6 },
chip: {
backgroundColor: colors.surface,
borderWidth: 1,
borderColor: colors.border,
borderRadius: 999,
paddingHorizontal: 10,
paddingVertical: 6,
},
chipText: { fontSize: 12, color: colors.text },
startBtn: {
marginHorizontal: 16,
marginTop: 24,
borderRadius: 14,
shadowColor: colors.primaryDark,
shadowOpacity: 0.25,
shadowRadius: 8,
shadowOffset: { width: 0, height: 4 },
elevation: 4,
},
startGradient: {
borderRadius: 14,
paddingVertical: 16,
alignItems: 'center',
},
startText: { color: colors.white, fontWeight: '800', fontSize: 16 },
footerNote: {
fontSize: 11,
color: colors.textMuted,
textAlign: 'center',
marginTop: 10,
},
pressed: { opacity: 0.85 },
});
+82
View File
@@ -18,12 +18,15 @@ import * as Sharing from 'expo-sharing';
import { colors } from '../theme/colors'; import { colors } from '../theme/colors';
import { getDb } from '../db/database'; import { getDb } from '../db/database';
import { import {
deleteCategory,
deleteRecurring, deleteRecurring,
getCategories,
getRecurring, getRecurring,
getSettingNumber, getSettingNumber,
setSetting, setSetting,
} from '../db/repositories'; } from '../db/repositories';
import { formatRupiah } from '../utils/format'; import { formatRupiah } from '../utils/format';
import type { Category, TransactionType } from '../types';
import type { RootStackParamList } from '../navigation'; import type { RootStackParamList } from '../navigation';
const PRESETS = [5, 10, 15, 20]; const PRESETS = [5, 10, 15, 20];
@@ -73,6 +76,7 @@ export function SettingsScreen() {
dayOfMonth: number; dayOfMonth: number;
}> }>
>([]); >([]);
const [categories, setCategories] = useState<Category[]>([]);
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
@@ -83,12 +87,33 @@ export function SettingsScreen() {
getRecurring().then((r) => { getRecurring().then((r) => {
if (active) setRecurring(r); if (active) setRecurring(r);
}); });
getCategories().then((c) => {
if (active) setCategories(c);
});
return () => { return () => {
active = false; active = false;
}; };
}, []) }, [])
); );
const handleDeleteCategory = (category: Category) => {
Alert.alert(
'Hapus kategori?',
`Kategori "${category.name}" akan dihapus. Transaksi lama tetap aman (jadi tanpa kategori).`,
[
{ text: 'Batal', style: 'cancel' },
{
text: 'Hapus',
style: 'destructive',
onPress: async () => {
await deleteCategory(category.id);
setCategories(await getCategories());
},
},
]
);
};
const handleDeleteRecurring = (id: number, label: string) => { const handleDeleteRecurring = (id: number, label: string) => {
Alert.alert( Alert.alert(
'Hapus transaksi berulang?', 'Hapus transaksi berulang?',
@@ -335,6 +360,55 @@ export function SettingsScreen() {
</Pressable> </Pressable>
</View> </View>
{/* Kategori */}
<Text style={styles.sectionTitle}>Kategori</Text>
<View style={[styles.card, styles.dataCard]}>
{(['income', 'expense'] as TransactionType[]).map((t) => (
<View key={t}>
<Text style={styles.catGroupLabel}>
{t === 'income' ? '💚 Pemasukan' : '❤️ Pengeluaran'}
</Text>
{categories
.filter((c) => c.type === t)
.map((c) => (
<View key={c.id} style={styles.recurringRow}>
<View style={styles.dataIconCircle}>
<Text style={styles.catEmoji}>{c.icon}</Text>
</View>
<Pressable
style={styles.dataInfo}
onPress={() =>
navigation.navigate('CategoryForm', {
categoryId: c.id,
initialName: c.name,
initialIcon: c.icon,
})
}
>
<Text style={styles.dataTitle}>{c.name}</Text>
</Pressable>
<Pressable onPress={() => handleDeleteCategory(c)} hitSlop={8}>
<Ionicons name="trash-outline" size={18} color={colors.expense} />
</Pressable>
</View>
))}
</View>
))}
<View style={styles.divider} />
<Pressable
style={({ pressed }) => [styles.dataRow, pressed && styles.pressed]}
onPress={() => navigation.navigate('CategoryForm', {})}
>
<View style={[styles.dataIconCircle, { backgroundColor: '#EDE9FE' }]}>
<Ionicons name="add" size={20} color="#7C3AED" />
</View>
<View style={styles.dataInfo}>
<Text style={styles.dataTitle}>Tambah kategori</Text>
<Text style={styles.dataHint}>Kategori sendiri dengan icon bebas</Text>
</View>
</Pressable>
</View>
{/* Data */} {/* Data */}
<Text style={styles.sectionTitle}>Data</Text> <Text style={styles.sectionTitle}>Data</Text>
<View style={[styles.card, styles.dataCard]}> <View style={[styles.card, styles.dataCard]}>
@@ -513,6 +587,14 @@ const styles = StyleSheet.create({
customButtonText: { color: colors.primaryDark, fontWeight: '700', fontSize: 13 }, customButtonText: { color: colors.primaryDark, fontWeight: '700', fontSize: 13 },
// Data // Data
dataCard: { marginBottom: 20 }, dataCard: { marginBottom: 20 },
catGroupLabel: {
fontSize: 12,
fontWeight: '700',
color: colors.textMuted,
marginTop: 6,
marginBottom: 4,
},
catEmoji: { fontSize: 17 },
emptyRecurringText: { emptyRecurringText: {
fontSize: 13, fontSize: 13,
color: colors.textMuted, color: colors.textMuted,