fitst commit

This commit is contained in:
Samandar Turgunboyev
2026-01-28 18:26:50 +05:00
parent 166a55b1e9
commit 124798419b
196 changed files with 26627 additions and 421 deletions

View 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',
},
});

View 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>
);
}

View 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';

View 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>
);
}

View 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>
);
}