This commit is contained in:
dios.one
2026-04-21 18:00:30 +07:00
parent 2173a765c9
commit 23dc306f12
67 changed files with 10302 additions and 67 deletions
+372
View File
@@ -0,0 +1,372 @@
import React, { useRef, useState, useEffect, useCallback } from 'react';
import {
View, Text, Image, TextInput, StyleSheet, FlatList,
Dimensions, TouchableOpacity, Animated,
KeyboardAvoidingView, Platform, Keyboard,
} from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import StarField from '../components/StarField';
import Button from '../components/Button';
import useAppStore from '../store/useAppStore';
import useAuthStore from '../store/useAuthStore';
import { colors, fonts, spacing, borderRadius } from '../utils/theme';
const { width } = Dimensions.get('window');
const ONBOARDING_KEY = 'onboarding_done';
const pages = [
{
id: '1',
title: 'Identity First',
text: 'Every transformation begins with identity.\nNot goals. Not habits. Identity.',
iconSize: 90,
glowIntensity: 0.3,
showOrbit: false,
},
{
id: '2',
title: '40 Days',
text: "In 40 days, you won't just change habits\n— you'll change who you are.",
iconSize: 110,
glowIntensity: 0.65,
showOrbit: false,
},
{
id: '3',
title: 'Your Nova Awaits',
text: 'Small actions. Massive transformation.',
iconSize: 110,
glowIntensity: 1,
showOrbit: true,
isLast: true,
},
];
function PageContent({ item, isActive }) {
const fadeAnim = useRef(new Animated.Value(0)).current;
const slideAnim = useRef(new Animated.Value(30)).current;
const pulse = useRef(new Animated.Value(1)).current;
useEffect(() => {
if (isActive) {
fadeAnim.setValue(0);
slideAnim.setValue(30);
Animated.parallel([
Animated.timing(fadeAnim, { toValue: 1, duration: 500, delay: 150, useNativeDriver: true }),
Animated.timing(slideAnim, { toValue: 0, duration: 500, delay: 150, useNativeDriver: true }),
]).start();
}
}, [isActive]);
useEffect(() => {
const anim = Animated.loop(
Animated.sequence([
Animated.timing(pulse, { toValue: 1.12, duration: 2200, useNativeDriver: true }),
Animated.timing(pulse, { toValue: 1, duration: 2200, useNativeDriver: true }),
])
);
anim.start();
return () => anim.stop();
}, []);
return (
<View style={styles.page}>
{/* Icon area */}
<View style={styles.iconArea}>
<Animated.View
style={[
styles.glow,
{
width: item.iconSize * 2.2,
height: item.iconSize * 2.2,
borderRadius: item.iconSize * 1.1,
opacity: item.glowIntensity * 0.4,
transform: [{ scale: pulse }],
},
]}
/>
{item.showOrbit && (
<View
style={[
styles.orbit,
{
width: item.iconSize * 2.6,
height: item.iconSize * 2.6,
borderRadius: item.iconSize * 1.3,
},
]}
/>
)}
<Image
source={require('../../assets/icon.png')}
style={{ width: item.iconSize * 1.2, height: item.iconSize * 1.2 }}
resizeMode="contain"
/>
</View>
{/* Text */}
<Animated.View style={{ opacity: fadeAnim, transform: [{ translateY: slideAnim }] }}>
<Text style={styles.pageTitle}>{item.title}</Text>
<Text style={styles.pageText}>{item.text}</Text>
</Animated.View>
</View>
);
}
export default function OnboardingScreen({ navigation }) {
const flatListRef = useRef(null);
const [currentIndex, setCurrentIndex] = useState(0);
const [identityInput, setIdentityInput] = useState('');
const [keyboardVisible, setKeyboardVisible] = useState(false);
const setInitialIdentity = useAppStore((s) => s.setInitialIdentity);
const buttonFade = useRef(new Animated.Value(0)).current;
const inputFade = useRef(new Animated.Value(0)).current;
const isLastPage = currentIndex === pages.length - 1;
useEffect(() => {
const showSub = Keyboard.addListener('keyboardWillShow', () => setKeyboardVisible(true));
const hideSub = Keyboard.addListener('keyboardWillHide', () => setKeyboardVisible(false));
// Android uses keyboardDidShow/keyboardDidHide
const showSubAnd = Keyboard.addListener('keyboardDidShow', () => setKeyboardVisible(true));
const hideSubAnd = Keyboard.addListener('keyboardDidHide', () => setKeyboardVisible(false));
return () => { showSub.remove(); hideSub.remove(); showSubAnd.remove(); hideSubAnd.remove(); };
}, []);
useEffect(() => {
if (isLastPage) {
Animated.stagger(200, [
Animated.timing(inputFade, { toValue: 1, duration: 400, useNativeDriver: true }),
Animated.timing(buttonFade, { toValue: 1, duration: 400, useNativeDriver: true }),
]).start();
} else {
buttonFade.setValue(0);
inputFade.setValue(0);
}
}, [isLastPage]);
const completeOnboarding = useCallback(async () => {
await AsyncStorage.setItem(ONBOARDING_KEY, 'true');
if (identityInput.trim()) {
setInitialIdentity(identityInput.trim());
}
// If user is already logged in (came from Register), go to story input
// Otherwise go to Login
const session = useAuthStore.getState().session;
if (session) {
navigation.reset({ index: 0, routes: [{ name: 'IdentityStory' }] });
} else {
navigation.reset({ index: 0, routes: [{ name: 'Login' }] });
}
}, [identityInput, navigation, setInitialIdentity]);
const onViewableItemsChanged = useRef(({ viewableItems }) => {
if (viewableItems.length > 0) {
setCurrentIndex(viewableItems[0].index ?? 0);
}
}).current;
return (
<View style={styles.container}>
<StarField count={60} />
{/* Skip button — hidden on last page */}
{!isLastPage && (
<TouchableOpacity style={styles.skip} onPress={completeOnboarding}>
<Text style={styles.skipText}>Skip</Text>
</TouchableOpacity>
)}
{/* Hide pager when keyboard is visible on last page to make room */}
{!(keyboardVisible && isLastPage) && (
<FlatList
ref={flatListRef}
data={pages}
horizontal
pagingEnabled
showsHorizontalScrollIndicator={false}
keyExtractor={(item) => item.id}
onViewableItemsChanged={onViewableItemsChanged}
viewabilityConfig={{ viewAreaCoveragePercentThreshold: 50 }}
renderItem={({ item, index }) => (
<PageContent item={item} isActive={index === currentIndex} />
)}
/>
)}
{/* Bottom section: input + button + dots */}
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
style={styles.bottomKAV}
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 0}
>
<View style={styles.bottom}>
{/* Show title when keyboard hides the pager */}
{keyboardVisible && isLastPage && (
<Text style={styles.keyboardTitle}>Who do you want to become?</Text>
)}
{/* Identity input on last page */}
{isLastPage && (
<Animated.View style={[styles.inputWrapper, { opacity: inputFade }]}>
{!keyboardVisible && (
<Text style={styles.inputLabel}>Who do you want to become?</Text>
)}
<TextInput
style={styles.input}
value={identityInput}
onChangeText={setIdentityInput}
placeholder="e.g., A disciplined, focused person"
placeholderTextColor={colors.textMuted}
selectionColor={colors.primary}
returnKeyType="done"
onSubmitEditing={() => Keyboard.dismiss()}
/>
</Animated.View>
)}
{/* CTA button on last page */}
{isLastPage && (
<Animated.View style={[styles.ctaWrapper, { opacity: buttonFade }]}>
<Button title="Get Started" onPress={completeOnboarding} />
</Animated.View>
)}
{/* Dots */}
{!keyboardVisible && (
<View style={styles.indicators}>
{pages.map((_, i) => (
<View
key={i}
style={[
styles.dot,
currentIndex === i && styles.dotActive,
]}
/>
))}
</View>
)}
</View>
</KeyboardAvoidingView>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
skip: {
position: 'absolute',
top: 58,
right: 24,
zIndex: 10,
paddingVertical: spacing.xs,
paddingHorizontal: spacing.sm,
},
skipText: {
color: colors.textSecondary,
fontSize: fonts.sizes.md,
fontWeight: fonts.weights.medium,
},
page: {
width,
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: spacing.xl,
},
iconArea: {
alignItems: 'center',
justifyContent: 'center',
width: 300,
height: 300,
marginBottom: spacing.xl,
},
glow: {
backgroundColor: colors.planetGlow,
position: 'absolute',
},
orbit: {
borderWidth: 1.5,
borderColor: colors.orbitLine,
borderStyle: 'dashed',
position: 'absolute',
},
pageTitle: {
color: colors.accent,
fontSize: fonts.sizes.xs,
fontWeight: fonts.weights.bold,
letterSpacing: 3,
textAlign: 'center',
textTransform: 'uppercase',
marginBottom: spacing.md,
},
pageText: {
color: colors.text,
fontSize: fonts.sizes.xl,
fontWeight: fonts.weights.semibold,
textAlign: 'center',
lineHeight: 34,
},
bottomKAV: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
},
bottom: {
paddingHorizontal: spacing.lg,
paddingBottom: spacing.xl,
paddingTop: spacing.md,
},
keyboardTitle: {
color: colors.text,
fontSize: fonts.sizes.lg,
fontWeight: fonts.weights.semibold,
textAlign: 'center',
marginBottom: spacing.md,
},
inputWrapper: {
marginBottom: spacing.lg,
},
inputLabel: {
color: colors.textSecondary,
fontSize: fonts.sizes.sm,
fontWeight: fonts.weights.medium,
marginBottom: spacing.sm,
textAlign: 'center',
},
input: {
backgroundColor: colors.surface,
borderRadius: borderRadius.md,
paddingHorizontal: spacing.md,
paddingVertical: spacing.md,
color: colors.text,
fontSize: fonts.sizes.md,
borderWidth: 1,
borderColor: colors.border,
textAlign: 'center',
},
ctaWrapper: {
marginBottom: spacing.md,
},
indicators: {
flexDirection: 'row',
justifyContent: 'center',
gap: spacing.sm,
paddingBottom: spacing.sm,
},
dot: {
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: colors.textMuted,
},
dotActive: {
backgroundColor: colors.primary,
width: 24,
},
});