Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
273 changes: 152 additions & 121 deletions apps/mobile/app/(onboarding)/getting-started.tsx
Original file line number Diff line number Diff line change
@@ -1,196 +1,227 @@
import { useEffect, useRef } from 'react';
import { View, Text, Pressable, Image, Animated } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { router } from 'expo-router';
import { View, Text, Pressable, Animated } from 'react-native';
import {
SafeAreaView,
SafeAreaProvider,
} from 'react-native-safe-area-context';
import { Ionicons } from '@expo/vector-icons';

export default function GettingStartedScreen() {
const fadeAnim = useRef(new Animated.Value(0)).current;
const slideAnim = useRef(new Animated.Value(50)).current;
const logoScale = useRef(new Animated.Value(0.5)).current;
const logoRotate = useRef(new Animated.Value(0)).current;
const titleSlide = useRef(new Animated.Value(30)).current;
export default function App() {
const containerFade = useRef(new Animated.Value(0)).current;
const containerSlide = useRef(new Animated.Value(40)).current;

const logoScale = useRef(new Animated.Value(0.85)).current;
const logoIdle = useRef(new Animated.Value(1)).current;

const titleFade = useRef(new Animated.Value(0)).current;
const descriptionSlide = useRef(new Animated.Value(20)).current;
const descriptionFade = useRef(new Animated.Value(0)).current;
const buttonScale = useRef(new Animated.Value(0.9)).current;
const buttonOpacity = useRef(new Animated.Value(0)).current;
const buttonSlide = useRef(new Animated.Value(20)).current;
const footerFade = useRef(new Animated.Value(0)).current;
const titleSlide = useRef(new Animated.Value(24)).current;

// Add breathing effect to logo after initial animation
const breathingAnimation = useRef(new Animated.Value(1)).current;
const descFade = useRef(new Animated.Value(0)).current;
const descSlide = useRef(new Animated.Value(20)).current;

const buttonFade = useRef(new Animated.Value(0)).current;
const buttonSlide = useRef(new Animated.Value(16)).current;

const footerFade = useRef(new Animated.Value(0)).current;

useEffect(() => {
// Main animation sequence
Animated.sequence([
// Stage 1: Container fade in and logo entrance
Animated.parallel([
Animated.timing(fadeAnim, {
Animated.timing(containerFade, {
toValue: 1,
duration: 800,
duration: 600,
useNativeDriver: true,
}),
Animated.timing(slideAnim, {
Animated.timing(containerSlide, {
toValue: 0,
duration: 800,
duration: 600,
useNativeDriver: true,
}),
Animated.spring(logoScale, {
toValue: 1,
tension: 40,
friction: 8,
useNativeDriver: true,
}),
Animated.timing(logoRotate, {
toValue: 1,
duration: 800,
tension: 30,
friction: 10,
useNativeDriver: true,
}),
]),

// Stage 2: Title animation
Animated.parallel([
Animated.timing(titleFade, {
toValue: 1,
duration: 600,
duration: 500,
useNativeDriver: true,
}),
Animated.spring(titleSlide, {
toValue: 0,
tension: 50,
friction: 8,
tension: 40,
friction: 9,
useNativeDriver: true,
}),
]),

// Stage 3: Description animation
Animated.parallel([
Animated.timing(descriptionFade, {
Animated.timing(descFade, {
toValue: 1,
duration: 500,
duration: 450,
useNativeDriver: true,
}),
Animated.spring(descriptionSlide, {
Animated.spring(descSlide, {
toValue: 0,
tension: 50,
friction: 8,
tension: 40,
friction: 9,
useNativeDriver: true,
}),
]),
]).start(() => {
// Stage 4: Button and footer animation
Animated.parallel([
Animated.spring(buttonScale, {
Animated.timing(buttonFade, {
toValue: 1,
tension: 60,
friction: 7,
useNativeDriver: true,
}),
Animated.timing(buttonOpacity, {
toValue: 1,
duration: 500,
duration: 400,
useNativeDriver: true,
}),
Animated.timing(buttonSlide, {
toValue: 0,
duration: 500,
duration: 400,
useNativeDriver: true,
}),
Animated.timing(footerFade, {
toValue: 1,
duration: 600,
delay: 200,
duration: 400,
delay: 100,
useNativeDriver: true,
}),
]).start(() => {
// Start breathing animation for logo after everything loads
]),
]).start(() => {
setTimeout(() => {
Animated.loop(
Animated.sequence([
Animated.timing(breathingAnimation, {
toValue: 1.05,
duration: 2000,
Animated.timing(logoIdle, {
toValue: 1.03,
duration: 3000,
useNativeDriver: true,
}),
Animated.timing(breathingAnimation, {
Animated.timing(logoIdle, {
toValue: 1,
duration: 2000,
duration: 3000,
useNativeDriver: true,
}),
])
).start();
});
}, 800);
});
}, []);

const logoRotateInterpolate = logoRotate.interpolate({
inputRange: [0, 1],
outputRange: ['-10deg', '0deg'],
});

return (
<SafeAreaView className="flex-1 bg-white dark:bg-gray-950" edges={['top', 'left', 'right']}>
<Animated.View
className="flex-1 items-center justify-center px-6"
style={{
opacity: fadeAnim,
transform: [{ translateY: slideAnim }],
}}>
<Animated.Image
source={require('../../assets/adaptive-icon.png')}
<SafeAreaProvider>
<SafeAreaView style={{ flex: 1, backgroundColor: 'white' }}>
<Animated.View
style={{
width: 150,
height: 150,
marginBottom: 32,
transform: [
{ scale: Animated.multiply(logoScale, breathingAnimation) },
{ rotate: logoRotateInterpolate },
],
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 24,
opacity: containerFade,
transform: [{ translateY: containerSlide }],
}}
resizeMode="contain"
/>
>
<Animated.Image
source={{
uri: 'https://docs.expo.dev/static/images/tutorial/background.png',
}}
resizeMode="contain"
style={{
width: 140,
height: 140,
marginBottom: 28,
transform: [{ scale: Animated.multiply(logoScale, logoIdle) }],
}}
/>

<Animated.View
style={{
opacity: titleFade,
transform: [{ translateY: titleSlide }],
}}>
<Text className="mb-4 text-center text-4xl font-extrabold tracking-tight text-gray-900 dark:text-white">
Learn Smarter with <Text className="text-orange-500">Blob</Text>
</Text>
<Animated.View
style={{
opacity: titleFade,
transform: [{ translateY: titleSlide }],
}}
>
<Text
style={{
fontSize: 32,
fontWeight: '800',
textAlign: 'center',
color: '#111',
}}
>
Learn Smarter with <Text style={{ color: '#f97316' }}>Blob</Text>
</Text>
</Animated.View>

<Animated.View
style={{
opacity: descFade,
transform: [{ translateY: descSlide }],
}}
>
<Text
style={{
marginTop: 12,
fontSize: 16,
color: '#666',
textAlign: 'center',
lineHeight: 22,
}}
>
Your AI-powered study companion. Transform notes into interactive
flashcards and quizzes instantly.
</Text>
</Animated.View>
</Animated.View>

<Animated.View
style={{
opacity: descriptionFade,
transform: [{ translateY: descriptionSlide }],
}}>
<Text className="px-5 text-center text-lg leading-6 text-gray-500 dark:text-gray-400">
Your AI-powered study companion. Transform notes into interactive flashcards and quizzes
instantly.
</Text>
</Animated.View>
</Animated.View>

<Animated.View
className="px-6 pb-8"
style={{
transform: [{ scale: buttonScale }, { translateY: buttonSlide }],
opacity: buttonOpacity,
}}>
<Pressable
className="mb-4 h-14 flex-row items-center justify-center rounded-2xl bg-orange-500 shadow-sm active:bg-orange-600 dark:bg-orange-600 dark:active:bg-orange-700"
onPress={() => router.push('/(onboarding)/login')}>
<Text className="mr-2 text-lg font-semibold text-white">Continue</Text>
<Ionicons name="arrow-forward" size={18} color="white" />
</Pressable>
paddingHorizontal: 24,
paddingBottom: 32,
opacity: buttonFade,
transform: [{ translateY: buttonSlide }],
}}
>
<Pressable
style={{
height: 56,
backgroundColor: '#f97316',
borderRadius: 18,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
}}
onPress={() => console.log('Continue pressed')}
>
<Text
style={{
color: 'white',
fontSize: 18,
fontWeight: '600',
}}
>
Continue
</Text>
<Ionicons
name="arrow-forward"
size={18}
color="white"
style={{ marginLeft: 8 }}
/>
</Pressable>

<Animated.View style={{ opacity: footerFade }}>
<Text className="px-4 text-center text-xs leading-5 text-gray-400 dark:text-gray-500">
<Animated.Text
style={{
marginTop: 14,
fontSize: 11,
textAlign: 'center',
color: '#999',
opacity: footerFade,
}}
>
By continuing, you agree to our Terms and Privacy Policy
</Text>
</Animated.Text>
</Animated.View>
</Animated.View>
</SafeAreaView>
</SafeAreaView>
</SafeAreaProvider>
);
}