diff --git a/App.tsx b/App.tsx index f6dca33..5845439 100644 --- a/App.tsx +++ b/App.tsx @@ -1,15 +1,19 @@ import { useEffect, useState } from 'react'; import { ActivityIndicator, StatusBar, StyleSheet, Text, View } from 'react-native'; +import * as SplashScreen from 'expo-splash-screen'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { NavigationContainer } from '@react-navigation/native'; 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 { colors } from './src/theme/colors'; +SplashScreen.preventAutoHideAsync().catch(() => {}); + export default function App() { const [ready, setReady] = useState(false); const [error, setError] = useState(null); + const [initialRoute, setInitialRoute] = useState<'Main' | 'Onboarding'>('Onboarding'); useEffect(() => { initDatabase() @@ -20,7 +24,10 @@ export default function App() { } catch { // jangan sampai mengganggu startup } + const onboarded = await getSetting('onboarded'); + setInitialRoute(onboarded === '1' ? 'Main' : 'Onboarding'); setReady(true); + SplashScreen.hideAsync().catch(() => {}); }) .catch((e) => setError(String(e))); }, []); @@ -46,7 +53,7 @@ export default function App() { return ( - + + + ([]); const [compare, setCompare] = useState({ current: 0, previous: 0 }); const [hidden, setHidden] = useState(true); + const [userName, setUserName] = useState(''); + const [userGender, setUserGender] = useState(''); const heartScale = useRef(new Animated.Value(1)).current; const hpAnim = useRef(new Animated.Value(0)).current; const shimmerX = useRef(new Animated.Value(-140)).current; @@ -85,7 +88,7 @@ export function DashboardScreen() { } | null>(null); const load = useCallback(async () => { - const [b, s, st, t, r, c, m, top, topLast] = await Promise.all([ + const [b, s, st, t, r, c, m, top, topLast, un, ug] = await Promise.all([ getBalance(), getSavingsTotal(), getSavingsStreak(), @@ -95,6 +98,8 @@ export function DashboardScreen() { getMonthSummary(), getTopExpenseCategory(), getTopExpenseCategory('-1 month'), + getSetting('user_name'), + getSetting('user_gender'), ]); setBalance(b); setSavings(s); @@ -106,6 +111,8 @@ export function DashboardScreen() { setMonthIncome(m.income); setTopCategory(top); setTopCategoryLast(topLast); + setUserName(un ?? ''); + setUserGender(ug ?? ''); }, []); useFocusEffect( @@ -267,12 +274,22 @@ export function DashboardScreen() { resizeMode="contain" /> - Halo, Mas 👋 + Halo, {userName || 'Mas'} 👋 {todayLabel()} - - N - + {userGender === 'female' ? ( + + ) : ( + + )} @@ -701,15 +718,10 @@ const styles = StyleSheet.create({ headerText: { flex: 1 }, greeting: { fontSize: 22, fontWeight: '800', color: colors.text }, date: { fontSize: 13, color: colors.textMuted, marginTop: 2 }, - avatarCircle: { - width: 40, - height: 40, - borderRadius: 20, - backgroundColor: colors.primary, - alignItems: 'center', - justifyContent: 'center', + avatarImg: { + width: 46, + height: 52, }, - avatarText: { color: colors.white, fontWeight: '800', fontSize: 16 }, quoteBubble: { flexDirection: 'row', alignItems: 'center', diff --git a/src/screens/OnboardingScreen.tsx b/src/screens/OnboardingScreen.tsx new file mode 100644 index 0000000..6d1e49f --- /dev/null +++ b/src/screens/OnboardingScreen.tsx @@ -0,0 +1,313 @@ +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 { + Image, + KeyboardAvoidingView, + Platform, + Pressable, + ScrollView, + StyleSheet, + Text, + TextInput, + View, +} from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { colors } from '../theme/colors'; +import { setSetting } from '../db/repositories'; +import type { RootStackParamList } from '../navigation'; + +export function OnboardingScreen() { + const navigation = + useNavigation>(); + const [step, setStep] = useState<1 | 2>(1); + const [name, setName] = useState(''); + const [gender, setGender] = useState<'male' | 'female' | null>(null); + + const handleFinish = async () => { + await setSetting('user_name', name.trim()); + await setSetting('user_gender', gender ?? 'male'); + await setSetting('onboarded', '1'); + navigation.replace('Main'); + }; + + return ( + + + + {/* Indikator langkah */} + + + + + + {step === 1 ? ( + <> + + + + + Halo kak! 👋 + + Selamat datang di Kantongku — asisten keuangan pribadimu. + + + + Panggilan Kakak siapa? + name.trim() && setStep(2)} + /> + [ + styles.nextBtn, + !name.trim() && styles.nextBtnDisabled, + pressed && styles.pressed, + ]} + disabled={!name.trim()} + onPress={() => setStep(2)} + > + Lanjut + + + + ) : ( + <> + + + + + Kak {name.trim()}, Cowok atau Cewek nih? 🤔 + + + Pilih avatar yang paling mirip kamu — biar Kantongku makin + personal. + + + + + setGender('male')} + > + + Cowok + {gender === 'male' && ( + + + + )} + + setGender('female')} + > + + Cewek + {gender === 'female' && ( + + + + )} + + + + [ + styles.nextBtn, + !gender && styles.nextBtnDisabled, + pressed && styles.pressed, + ]} + disabled={!gender} + onPress={handleFinish} + > + 🚀 Mulai Pakai Kantongku + + + )} + + + + ); +} + +const styles = StyleSheet.create({ + safe: { flex: 1, backgroundColor: colors.background }, + flex: { flex: 1 }, + content: { padding: 16, paddingBottom: 32, flexGrow: 1 }, + steps: { + flexDirection: 'row', + justifyContent: 'center', + gap: 8, + marginVertical: 16, + }, + stepDot: { + width: 8, + height: 8, + borderRadius: 4, + backgroundColor: colors.border, + }, + stepDotActive: { + backgroundColor: colors.primary, + width: 24, + }, + hero: { + borderRadius: 24, + padding: 24, + 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: 76, height: 76, marginBottom: 10 }, + heroTitle: { + fontSize: 24, + fontWeight: '800', + color: colors.white, + }, + heroTitle2: { + fontSize: 20, + fontWeight: '800', + color: colors.white, + textAlign: 'center', + lineHeight: 27, + }, + heroSub: { + fontSize: 13, + color: 'rgba(255,255,255,0.9)', + textAlign: 'center', + lineHeight: 19, + marginTop: 8, + }, + question: { + fontSize: 17, + fontWeight: '700', + color: colors.text, + marginTop: 24, + marginBottom: 10, + }, + card: { + backgroundColor: colors.surface, + borderRadius: 16, + padding: 16, + borderWidth: 1, + borderColor: colors.border, + }, + input: { + paddingVertical: 14, + fontSize: 16, + color: colors.text, + marginBottom: 16, + }, + nextBtn: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gap: 6, + backgroundColor: colors.primary, + borderRadius: 14, + paddingVertical: 15, + marginTop: 8, + shadowColor: colors.primaryDark, + shadowOpacity: 0.25, + shadowRadius: 8, + shadowOffset: { width: 0, height: 4 }, + elevation: 4, + }, + nextBtnDisabled: { opacity: 0.4, elevation: 0, shadowOpacity: 0 }, + nextBtnText: { color: colors.white, fontWeight: '800', fontSize: 16 }, + genderRow: { flexDirection: 'row', gap: 12, marginTop: 24, marginBottom: 20 }, + genderCard: { + flex: 1, + alignItems: 'center', + backgroundColor: colors.surface, + borderWidth: 2, + borderColor: colors.border, + borderRadius: 20, + paddingVertical: 20, + position: 'relative', + }, + genderCardActive: { + borderColor: colors.primary, + backgroundColor: colors.primarySoft, + }, + avatar: { width: 110, height: 130 }, + genderLabel: { + fontSize: 16, + fontWeight: '800', + color: colors.text, + marginTop: 10, + }, + checkBadge: { + position: 'absolute', + top: 10, + right: 10, + width: 22, + height: 22, + borderRadius: 11, + backgroundColor: colors.primary, + alignItems: 'center', + justifyContent: 'center', + }, + pressed: { opacity: 0.85 }, +});