fitst commit
This commit is contained in:
432
screens/welcome/styles/welcomeStyle.ts
Normal file
432
screens/welcome/styles/welcomeStyle.ts
Normal file
@@ -0,0 +1,432 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
export const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#FAFBFF',
|
||||
},
|
||||
|
||||
// Header Styles
|
||||
header: {
|
||||
backgroundColor: '#fff',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#F0F2F8',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 8,
|
||||
elevation: 3,
|
||||
},
|
||||
headerContent: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 16,
|
||||
},
|
||||
logoBox: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
},
|
||||
logoCircle: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 21,
|
||||
backgroundColor: '#6366F1',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
shadowColor: '#6366F1',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 8,
|
||||
elevation: 5,
|
||||
},
|
||||
logoText: {
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
fontWeight: '800',
|
||||
},
|
||||
brandText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
color: '#1F2937',
|
||||
},
|
||||
headerRight: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
zIndex: 100,
|
||||
gap: 12,
|
||||
},
|
||||
|
||||
// Language Select
|
||||
langContainer: {
|
||||
position: 'relative',
|
||||
zIndex: 1000,
|
||||
height: 40,
|
||||
},
|
||||
langTrigger: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
position: 'relative',
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 12,
|
||||
backgroundColor: '#F9FAFB',
|
||||
borderWidth: 1,
|
||||
borderColor: '#E5E7EB',
|
||||
},
|
||||
langIcon: {
|
||||
fontSize: 16,
|
||||
},
|
||||
langText: {
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
color: '#374151',
|
||||
},
|
||||
chevron: {
|
||||
fontSize: 10,
|
||||
color: '#9CA3AF',
|
||||
},
|
||||
langMenu: {
|
||||
position: 'absolute',
|
||||
top: 48,
|
||||
right: 0,
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 14,
|
||||
borderWidth: 1,
|
||||
borderColor: '#E5E7EB',
|
||||
minWidth: 160,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 12,
|
||||
elevation: 20, // oshirdik
|
||||
zIndex: 1000, // iOS uchun
|
||||
},
|
||||
langItem: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
zIndex: 50,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#F3F4F6',
|
||||
},
|
||||
langItemActive: {
|
||||
backgroundColor: '#EEF2FF',
|
||||
},
|
||||
langItemText: {
|
||||
fontSize: 14,
|
||||
color: '#374151',
|
||||
},
|
||||
langItemTextActive: {
|
||||
color: '#6366F1',
|
||||
fontWeight: '600',
|
||||
},
|
||||
checkmark: {
|
||||
color: '#6366F1',
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
|
||||
// Menu Button
|
||||
menuBtn: {
|
||||
padding: 8,
|
||||
},
|
||||
hamburger: {
|
||||
width: 24,
|
||||
height: 20,
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
line: {
|
||||
width: 24,
|
||||
height: 2,
|
||||
backgroundColor: '#374151',
|
||||
borderRadius: 2,
|
||||
},
|
||||
lineRotate1: {
|
||||
transform: [{ rotate: '45deg' }, { translateY: 9 }],
|
||||
},
|
||||
lineHide: {
|
||||
opacity: 0,
|
||||
},
|
||||
lineRotate2: {
|
||||
transform: [{ rotate: '-45deg' }, { translateY: -9 }],
|
||||
},
|
||||
|
||||
// Mobile Menu
|
||||
mobileMenu: {
|
||||
backgroundColor: '#fff',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#E5E7EB',
|
||||
paddingVertical: 8,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 8,
|
||||
elevation: 3,
|
||||
},
|
||||
menuItem: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 20,
|
||||
marginHorizontal: 12,
|
||||
marginVertical: 4,
|
||||
borderRadius: 12,
|
||||
backgroundColor: '#F9FAFB',
|
||||
},
|
||||
menuItemPrimary: {
|
||||
backgroundColor: '#6366F1',
|
||||
},
|
||||
menuItemText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#374151',
|
||||
},
|
||||
menuItemTextPrimary: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#fff',
|
||||
},
|
||||
menuArrow: {
|
||||
fontSize: 18,
|
||||
color: '#9CA3AF',
|
||||
},
|
||||
menuArrowPrimary: {
|
||||
fontSize: 18,
|
||||
color: '#fff',
|
||||
},
|
||||
|
||||
// Scroll Content
|
||||
scrollContent: {
|
||||
paddingBottom: 20,
|
||||
},
|
||||
|
||||
// Hero Section
|
||||
hero: {
|
||||
paddingHorizontal: 20,
|
||||
marginTop: 20,
|
||||
paddingBottom: 32,
|
||||
alignItems: 'center',
|
||||
},
|
||||
badge: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 16,
|
||||
borderRadius: 20,
|
||||
backgroundColor: '#EEF2FF',
|
||||
borderWidth: 1,
|
||||
borderColor: '#DDD6FE',
|
||||
marginBottom: 24,
|
||||
},
|
||||
badgeDot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: '#6366F1',
|
||||
},
|
||||
badgeText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
color: '#6366F1',
|
||||
},
|
||||
title: {
|
||||
fontSize: 28,
|
||||
fontWeight: '800',
|
||||
color: '#1F2937',
|
||||
textAlign: 'center',
|
||||
lineHeight: 40,
|
||||
},
|
||||
titleGradient: {
|
||||
color: '#6366F1',
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 16,
|
||||
color: '#6B7280',
|
||||
textAlign: 'center',
|
||||
lineHeight: 24,
|
||||
},
|
||||
mainBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 12,
|
||||
backgroundColor: '#6366F1',
|
||||
paddingVertical: 16,
|
||||
paddingHorizontal: 32,
|
||||
borderRadius: 16,
|
||||
width: 'auto',
|
||||
shadowColor: '#6366F1',
|
||||
shadowOffset: { width: 0, height: 8 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 16,
|
||||
elevation: 8,
|
||||
marginTop: 20,
|
||||
},
|
||||
mainBtnText: {
|
||||
color: '#fff',
|
||||
fontSize: 17,
|
||||
fontWeight: '700',
|
||||
},
|
||||
btnArrow: {
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 14,
|
||||
backgroundColor: 'rgba(255,255,255,0.2)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
btnArrowText: {
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
|
||||
// Features
|
||||
features: {
|
||||
paddingHorizontal: 20,
|
||||
gap: 16,
|
||||
},
|
||||
card: {
|
||||
backgroundColor: '#fff',
|
||||
padding: 24,
|
||||
borderRadius: 20,
|
||||
borderWidth: 1,
|
||||
borderColor: '#F0F2F8',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 12,
|
||||
elevation: 3,
|
||||
},
|
||||
cardIcon: {
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 16,
|
||||
backgroundColor: '#EEF2FF',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
},
|
||||
cardIconText: {
|
||||
fontSize: 28,
|
||||
},
|
||||
cardTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
color: '#1F2937',
|
||||
marginBottom: 8,
|
||||
},
|
||||
cardDesc: {
|
||||
fontSize: 15,
|
||||
color: '#6B7280',
|
||||
lineHeight: 22,
|
||||
},
|
||||
|
||||
// Stats
|
||||
stats: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-around',
|
||||
alignItems: 'center',
|
||||
marginHorizontal: 20,
|
||||
marginTop: 32,
|
||||
padding: 24,
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 20,
|
||||
borderWidth: 1,
|
||||
borderColor: '#F0F2F8',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 12,
|
||||
elevation: 3,
|
||||
},
|
||||
stat: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
statNumber: {
|
||||
fontSize: 24,
|
||||
fontWeight: '800',
|
||||
color: '#6366F1',
|
||||
marginBottom: 4,
|
||||
},
|
||||
statLabel: {
|
||||
fontSize: 12,
|
||||
color: '#6B7280',
|
||||
fontWeight: '500',
|
||||
},
|
||||
statDivider: {
|
||||
width: 1,
|
||||
height: 40,
|
||||
backgroundColor: '#E5E7EB',
|
||||
},
|
||||
|
||||
// Footer
|
||||
footer: {
|
||||
position: 'fixed',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
flexDirection: 'column',
|
||||
gap: 12,
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 16,
|
||||
backgroundColor: '#fff',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#F0F2F8',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: -2 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 8,
|
||||
elevation: 5,
|
||||
height: 'auto',
|
||||
},
|
||||
footerBtnOutline: {
|
||||
flex: 1,
|
||||
minHeight: 50,
|
||||
paddingVertical: 1,
|
||||
borderRadius: 14,
|
||||
borderWidth: 1,
|
||||
borderColor: '#E5E7EB',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
footerBtnOutlineText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#374151',
|
||||
},
|
||||
footerBtnPrimary: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 14,
|
||||
backgroundColor: '#6366F1',
|
||||
shadowColor: '#6366F1',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 12,
|
||||
elevation: 5,
|
||||
},
|
||||
footerBtnPrimaryText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
color: '#fff',
|
||||
},
|
||||
footerBtnArrow: {
|
||||
fontSize: 18,
|
||||
color: '#fff',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
});
|
||||
56
screens/welcome/ui/FeatureCard.tsx
Normal file
56
screens/welcome/ui/FeatureCard.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { ThemedText } from '@/components/themed-text';
|
||||
import { useEffect } from 'react';
|
||||
import { Animated, View } from 'react-native';
|
||||
import { styles } from '../styles/welcomeStyle';
|
||||
|
||||
export default function FeatureCard({
|
||||
icon,
|
||||
title,
|
||||
desc,
|
||||
delay,
|
||||
bgColor,
|
||||
}: {
|
||||
icon: any;
|
||||
title: string;
|
||||
bgColor: string;
|
||||
desc: string;
|
||||
delay: number;
|
||||
}) {
|
||||
const fadeAnim = new Animated.Value(0);
|
||||
const slideAnim = new Animated.Value(30);
|
||||
|
||||
useEffect(() => {
|
||||
Animated.parallel([
|
||||
Animated.timing(fadeAnim, {
|
||||
toValue: 1,
|
||||
duration: 600,
|
||||
delay: delay,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(slideAnim, {
|
||||
toValue: 0,
|
||||
duration: 600,
|
||||
delay: delay,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.card,
|
||||
{
|
||||
opacity: fadeAnim,
|
||||
transform: [{ translateY: slideAnim }],
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View style={{ ...styles.cardIcon, backgroundColor: bgColor, borderRadius: 12 }}>
|
||||
<ThemedText style={styles.cardIconText}>{icon}</ThemedText>
|
||||
</View>
|
||||
<ThemedText style={styles.cardTitle}>{title}</ThemedText>
|
||||
<ThemedText style={styles.cardDesc}>{desc}</ThemedText>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
190
screens/welcome/ui/LanguageSelect.tsx
Normal file
190
screens/welcome/ui/LanguageSelect.tsx
Normal file
@@ -0,0 +1,190 @@
|
||||
import { ThemedText } from '@/components/themed-text';
|
||||
import Feather from '@expo/vector-icons/Feather';
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Animated, Modal, TouchableOpacity, View } from 'react-native';
|
||||
import { styles } from '../styles/welcomeStyle';
|
||||
|
||||
export default function LanguageSelect() {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [current, setCurrent] = useState('uz');
|
||||
const [menuPos, setMenuPos] = useState({ x: 0, y: 0, width: 0, height: 0 });
|
||||
const triggerRef = useRef<View>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchLang = async () => {
|
||||
const lang = await getLang();
|
||||
if (lang) setCurrent(lang);
|
||||
};
|
||||
fetchLang();
|
||||
}, []);
|
||||
|
||||
const backdropAnim = useRef(new Animated.Value(0)).current;
|
||||
|
||||
const fadeAnim = useRef(new Animated.Value(0)).current;
|
||||
const slideAnim = useRef(new Animated.Value(-20)).current;
|
||||
const scaleAnim = useRef(new Animated.Value(0.9)).current;
|
||||
|
||||
const languages = [
|
||||
{ key: 'uz', label: "🇺🇿 O'zbek" },
|
||||
{ key: 'ru', label: '🇷🇺 Русский' },
|
||||
{ key: 'en', label: '🇺🇸 English' },
|
||||
];
|
||||
|
||||
const openMenu = () => {
|
||||
triggerRef.current?.measureInWindow((x, y, width, height) => {
|
||||
setMenuPos({
|
||||
x,
|
||||
y: y + height + 60,
|
||||
width,
|
||||
height,
|
||||
});
|
||||
setVisible(true);
|
||||
});
|
||||
};
|
||||
|
||||
const closeMenu = () => {
|
||||
Animated.parallel([
|
||||
Animated.timing(backdropAnim, {
|
||||
toValue: 0,
|
||||
duration: 200,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(fadeAnim, {
|
||||
toValue: 0,
|
||||
duration: 150,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(slideAnim, {
|
||||
toValue: -20,
|
||||
duration: 150,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(scaleAnim, {
|
||||
toValue: 0.9,
|
||||
duration: 150,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start(() => {
|
||||
setVisible(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
backdropAnim.setValue(0);
|
||||
fadeAnim.setValue(0);
|
||||
slideAnim.setValue(-20);
|
||||
scaleAnim.setValue(0.9);
|
||||
|
||||
Animated.parallel([
|
||||
Animated.timing(backdropAnim, {
|
||||
toValue: 1,
|
||||
duration: 250,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(fadeAnim, {
|
||||
toValue: 1,
|
||||
duration: 250,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.spring(slideAnim, {
|
||||
toValue: 0,
|
||||
tension: 100,
|
||||
friction: 8,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.spring(scaleAnim, {
|
||||
toValue: 1,
|
||||
tension: 100,
|
||||
friction: 8,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start();
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
const selectLanguage = async (lang: string) => {
|
||||
closeMenu();
|
||||
setCurrent(lang);
|
||||
await i18n.changeLanguage(lang);
|
||||
await saveLang(lang);
|
||||
};
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View style={styles.langContainer} ref={triggerRef}>
|
||||
<TouchableOpacity style={styles.langTrigger} onPress={openMenu} activeOpacity={0.7}>
|
||||
<Feather name="globe" size={18} color="black" />
|
||||
<ThemedText style={{ ...styles.langText }}>{current.toUpperCase()}</ThemedText>
|
||||
</TouchableOpacity>
|
||||
|
||||
<Modal
|
||||
transparent
|
||||
visible={visible}
|
||||
animationType="none"
|
||||
statusBarTranslucent
|
||||
onRequestClose={closeMenu}
|
||||
>
|
||||
<TouchableOpacity style={{ flex: 1 }} activeOpacity={1} onPress={closeMenu}>
|
||||
<Animated.View
|
||||
style={{
|
||||
...StyleSheet.absoluteFillObject,
|
||||
opacity: backdropAnim,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Animated.View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: menuPos.x - menuPos.width - 20,
|
||||
top: menuPos.y,
|
||||
width: 160,
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 16,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 8 },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 16,
|
||||
elevation: 12,
|
||||
overflow: 'hidden',
|
||||
opacity: fadeAnim,
|
||||
transform: [{ translateY: slideAnim }, { scale: scaleAnim }],
|
||||
}}
|
||||
>
|
||||
{languages.map((l, index) => (
|
||||
<TouchableOpacity
|
||||
key={l.key}
|
||||
onPress={() => selectLanguage(l.key)}
|
||||
activeOpacity={0.7}
|
||||
style={{
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 16,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
backgroundColor: current === l.key ? '#EEF2FF' : 'transparent',
|
||||
borderBottomWidth: index < languages.length - 1 ? 1 : 0,
|
||||
borderBottomColor: '#F3F4F6',
|
||||
}}
|
||||
>
|
||||
<ThemedText
|
||||
style={[styles.langItemText, current === l.key && styles.langItemTextActive]}
|
||||
>
|
||||
{l.label}
|
||||
</ThemedText>
|
||||
{current === l.key && <MaterialIcons name="done" size={20} color="#6366F1" />}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
</Modal>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// StyleSheet.absoluteFillObject uchun import qo'shing:
|
||||
import { getLang, saveLang } from '@/hooks/storage.native';
|
||||
import i18n from '@/i18n/i18n';
|
||||
import { StyleSheet } from 'react-native';
|
||||
218
screens/welcome/ui/Welcome.tsx
Normal file
218
screens/welcome/ui/Welcome.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
import { ThemedText } from '@/components/themed-text';
|
||||
import AntDesign from '@expo/vector-icons/AntDesign';
|
||||
import Feather from '@expo/vector-icons/Feather';
|
||||
import MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Animated, RefreshControl, ScrollView, TouchableOpacity, View } from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { styles } from '../styles/welcomeStyle';
|
||||
import FeatureCard from './FeatureCard';
|
||||
import WelcomeHeader from './WelcomeHeader';
|
||||
|
||||
export default function WelcomePage() {
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const fadeAnim = new Animated.Value(0);
|
||||
const router = useRouter();
|
||||
const slideAnim = new Animated.Value(50);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const menuHeightAnim = useRef(new Animated.Value(0)).current;
|
||||
const menuOpacityAnim = useRef(new Animated.Value(0)).current;
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
Animated.parallel([
|
||||
Animated.timing(fadeAnim, {
|
||||
toValue: 1,
|
||||
duration: 800,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(slideAnim, {
|
||||
toValue: 0,
|
||||
duration: 800,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start();
|
||||
}, []);
|
||||
|
||||
// Menu animatsiyasi
|
||||
useEffect(() => {
|
||||
if (menuOpen) {
|
||||
Animated.parallel([
|
||||
Animated.spring(menuHeightAnim, {
|
||||
toValue: 1,
|
||||
tension: 80,
|
||||
friction: 10,
|
||||
useNativeDriver: false,
|
||||
}),
|
||||
Animated.timing(menuOpacityAnim, {
|
||||
toValue: 1,
|
||||
duration: 250,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start();
|
||||
} else {
|
||||
Animated.parallel([
|
||||
Animated.timing(menuHeightAnim, {
|
||||
toValue: 0,
|
||||
duration: 200,
|
||||
useNativeDriver: false,
|
||||
}),
|
||||
Animated.timing(menuOpacityAnim, {
|
||||
toValue: 0,
|
||||
duration: 150,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start();
|
||||
}
|
||||
}, [menuOpen]);
|
||||
|
||||
const handleNavigation = (screen: 'announcements' | '(tabs)/login') => {
|
||||
setMenuOpen(false);
|
||||
router.push(`/${screen}`);
|
||||
};
|
||||
|
||||
const onRefresh = () => {
|
||||
setRefreshing(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setRefreshing(false);
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<WelcomeHeader onMenuPress={() => setMenuOpen(!menuOpen)} menuOpen={menuOpen} />
|
||||
|
||||
{menuOpen && (
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.mobileMenu,
|
||||
{
|
||||
opacity: menuOpacityAnim,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<TouchableOpacity style={styles.menuItem} activeOpacity={0.7}>
|
||||
<ThemedText style={styles.menuItemText}>{t('mainPage.sign_in')}</ThemedText>
|
||||
<AntDesign name="right" size={15} color="black" />
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity style={[styles.menuItem]} activeOpacity={0.7}>
|
||||
<ThemedText style={styles.menuItemText}>{t('mainPage.sign_up')}</ThemedText>
|
||||
<AntDesign name="right" size={15} color="black" />
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
)}
|
||||
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
tintColor="#155dfc"
|
||||
colors={['#155dfc']}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<View style={[styles.hero]}>
|
||||
<View style={styles.badge}>
|
||||
<ThemedText style={styles.badgeText}> {t('mainPage.safe')}</ThemedText>
|
||||
</View>
|
||||
|
||||
<ThemedText style={styles.title}>{t('mainPage.welcome')}</ThemedText>
|
||||
|
||||
<ThemedText style={styles.subtitle}>{t('mainPage.welcome_desc')}</ThemedText>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.mainBtn}
|
||||
activeOpacity={0.9}
|
||||
onPress={() => handleNavigation('announcements')}
|
||||
>
|
||||
<ThemedText style={styles.mainBtnText}> {t('common.enrol')}</ThemedText>
|
||||
<View style={styles.btnArrow}>
|
||||
<AntDesign name="right" size={14} color="white" />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.features}>
|
||||
<FeatureCard
|
||||
bgColor="#dbeafe"
|
||||
icon={<MaterialCommunityIcons name="shield-outline" size={24} color="#155dfc" />}
|
||||
title={t('mainPage.card1Title')}
|
||||
desc={t('mainPage.card1Desc')}
|
||||
delay={200}
|
||||
/>
|
||||
<FeatureCard
|
||||
bgColor="#dbeafe"
|
||||
icon={<Feather name="zap" size={24} color="#155dfc" />}
|
||||
title={t('mainPage.card2Title')}
|
||||
desc={t('mainPage.card2Desc')}
|
||||
delay={300}
|
||||
/>
|
||||
<FeatureCard
|
||||
bgColor="#f3e8ff"
|
||||
icon={<Feather name="users" size={24} color="#9810fa" />}
|
||||
title={t('mainPage.card3Title')}
|
||||
desc={t('mainPage.card3Desc')}
|
||||
delay={400}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
<View style={styles.footer}>
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
...styles.footerBtnOutline,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 24,
|
||||
borderRadius: 12,
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<ThemedText style={styles.footerBtnOutlineText}>{t('mainPage.enter')}</ThemedText>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity onPress={() => handleNavigation('(tabs)/login')} activeOpacity={0.9}>
|
||||
<LinearGradient
|
||||
colors={['#3b82f6', '#6366f1']}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 24,
|
||||
borderRadius: 12,
|
||||
marginBottom: 10,
|
||||
}}
|
||||
>
|
||||
<ThemedText
|
||||
style={{
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
marginRight: 8,
|
||||
}}
|
||||
>
|
||||
{t('mainPage.start')}
|
||||
</ThemedText>
|
||||
<AntDesign name="right" size={14} color="white" />
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
109
screens/welcome/ui/WelcomeHeader.tsx
Normal file
109
screens/welcome/ui/WelcomeHeader.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { ThemedText } from '@/components/themed-text';
|
||||
import AntDesign from '@expo/vector-icons/AntDesign';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Animated, TouchableOpacity, View } from 'react-native';
|
||||
import { styles } from '../styles/welcomeStyle';
|
||||
import LanguageSelect from './LanguageSelect';
|
||||
|
||||
export default function WelcomeHeader({
|
||||
onMenuPress,
|
||||
menuOpen,
|
||||
}: {
|
||||
onMenuPress: any;
|
||||
menuOpen: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Icon rotation animatsiyasi
|
||||
const rotateAnim = useRef(new Animated.Value(0)).current;
|
||||
const scaleAnim = useRef(new Animated.Value(1)).current;
|
||||
|
||||
useEffect(() => {
|
||||
if (menuOpen) {
|
||||
Animated.parallel([
|
||||
Animated.spring(rotateAnim, {
|
||||
toValue: 1,
|
||||
tension: 100,
|
||||
friction: 8,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.sequence([
|
||||
Animated.timing(scaleAnim, {
|
||||
toValue: 0.8,
|
||||
duration: 100,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.spring(scaleAnim, {
|
||||
toValue: 1,
|
||||
tension: 100,
|
||||
friction: 5,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]),
|
||||
]).start();
|
||||
} else {
|
||||
Animated.parallel([
|
||||
Animated.spring(rotateAnim, {
|
||||
toValue: 0,
|
||||
tension: 100,
|
||||
friction: 8,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.sequence([
|
||||
Animated.timing(scaleAnim, {
|
||||
toValue: 0.8,
|
||||
duration: 100,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.spring(scaleAnim, {
|
||||
toValue: 1,
|
||||
tension: 100,
|
||||
friction: 5,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]),
|
||||
]).start();
|
||||
}
|
||||
}, [menuOpen]);
|
||||
|
||||
// Rotation interpolation
|
||||
const rotation = rotateAnim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: ['0deg', '90deg'],
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={styles.header}>
|
||||
<View style={styles.headerContent}>
|
||||
<View style={styles.logoBox}>
|
||||
<View style={styles.logoCircle}>
|
||||
<ThemedText style={styles.logoText}>IT</ThemedText>
|
||||
</View>
|
||||
<ThemedText style={styles.brandText}>{t('common.target')}</ThemedText>
|
||||
</View>
|
||||
|
||||
<View style={styles.headerRight}>
|
||||
<LanguageSelect />
|
||||
|
||||
<TouchableOpacity onPress={onMenuPress} style={styles.menuBtn} activeOpacity={0.7}>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.hamburger,
|
||||
{
|
||||
transform: [{ rotate: rotation }, { scale: scaleAnim }],
|
||||
},
|
||||
]}
|
||||
>
|
||||
{menuOpen ? (
|
||||
<AntDesign name="close" size={22} color="#374151" />
|
||||
) : (
|
||||
<AntDesign name="menu" size={22} color="#374151" />
|
||||
)}
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user