Initial commit
This commit is contained in:
11
src/screens/auth/login/lib/form.ts
Normal file
11
src/screens/auth/login/lib/form.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
// form.ts
|
||||
import { z } from 'zod';
|
||||
|
||||
export const loginSchema = z.object({
|
||||
phone: z.string().min(12, 'Xato raqam kiritildi'),
|
||||
passportSeriya: z.string().length(2, '2 ta harf kerak'),
|
||||
passportNumber: z.string().length(7, '7 ta raqam kerak'),
|
||||
branchId: z.number().min(1, 'Filialni tanlang'),
|
||||
});
|
||||
|
||||
export type LoginFormType = z.infer<typeof loginSchema>;
|
||||
32
src/screens/auth/login/lib/userstore.ts
Normal file
32
src/screens/auth/login/lib/userstore.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface UserState {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
phoneNumber: string;
|
||||
expireTime: number;
|
||||
setExpireTime: (time: number) => void;
|
||||
setUser: (user: Partial<UserState>) => void;
|
||||
clearUser: () => void;
|
||||
}
|
||||
|
||||
export const useUserStore = create<UserState>(set => ({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
phoneNumber: '',
|
||||
expireTime: 0,
|
||||
setExpireTime: time => set({ expireTime: time }),
|
||||
|
||||
setUser: user =>
|
||||
set(state => ({
|
||||
...state,
|
||||
...user,
|
||||
})),
|
||||
|
||||
clearUser: () =>
|
||||
set({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
phoneNumber: '',
|
||||
}),
|
||||
}));
|
||||
321
src/screens/auth/login/ui/Confirm.tsx
Normal file
321
src/screens/auth/login/ui/Confirm.tsx
Normal file
@@ -0,0 +1,321 @@
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { authApi } from 'api/auth';
|
||||
import { otpPayload, resendPayload } from 'api/auth/type';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
ImageBackground,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import SimpleLineIcons from 'react-native-vector-icons/SimpleLineIcons';
|
||||
import Logo from 'screens/../../assets/bootsplash/logo_512.png';
|
||||
import { useModalStore } from 'screens/auth/registeration/lib/modalStore';
|
||||
import LanguageSelector from 'screens/auth/select-language/SelectLang';
|
||||
import { RootStackParamList } from 'types/types';
|
||||
import { useUserStore } from '../lib/userstore';
|
||||
import { Loginstyle } from './styled';
|
||||
|
||||
type VerificationCodeScreenNavigationProp = NativeStackNavigationProp<
|
||||
RootStackParamList,
|
||||
'Confirm'
|
||||
>;
|
||||
|
||||
const OTP_LENGTH = 4;
|
||||
|
||||
const Confirm = () => {
|
||||
const navigation = useNavigation<VerificationCodeScreenNavigationProp>();
|
||||
const { t } = useTranslation();
|
||||
const [code, setCode] = useState<string[]>(new Array(OTP_LENGTH).fill(''));
|
||||
const [timer, setTimer] = useState(60);
|
||||
const [errorConfirm, setErrorConfirm] = useState<string | null>(null);
|
||||
const [canResend, setCanResend] = useState(false);
|
||||
const inputRefs = useRef<Array<TextInput | null>>([]);
|
||||
const { phoneNumber } = useUserStore(state => state);
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: (payload: otpPayload) => authApi.verifyOtp(payload),
|
||||
onSuccess: async res => {
|
||||
await AsyncStorage.setItem('token', res.data.accessToken);
|
||||
navigation.navigate('Home');
|
||||
setErrorConfirm(null);
|
||||
},
|
||||
onError: (err: any) => {
|
||||
setErrorConfirm(err?.response.data.message);
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: resendMutate } = useMutation({
|
||||
mutationFn: (payload: resendPayload) => authApi.resendOtp(payload),
|
||||
onSuccess: async res => {
|
||||
setTimer(60);
|
||||
setCanResend(false);
|
||||
setCode(new Array(OTP_LENGTH).fill(''));
|
||||
inputRefs.current[0]?.focus();
|
||||
setErrorConfirm(null);
|
||||
},
|
||||
onError: (err: any) => {
|
||||
setErrorConfirm(err?.response.data.message);
|
||||
},
|
||||
});
|
||||
|
||||
const openModal = useModalStore(state => state.openModal);
|
||||
useEffect(() => {
|
||||
let interval: NodeJS.Timeout | null = null;
|
||||
if (timer > 0) {
|
||||
interval = setInterval(() => {
|
||||
setTimer(prevTimer => prevTimer - 1);
|
||||
}, 1000);
|
||||
} else {
|
||||
setCanResend(true);
|
||||
if (interval) clearInterval(interval);
|
||||
}
|
||||
return () => {
|
||||
if (interval) clearInterval(interval);
|
||||
};
|
||||
}, [timer]);
|
||||
|
||||
const handleCodeChange = (text: string, index: number) => {
|
||||
const newCode = [...code];
|
||||
newCode[index] = text;
|
||||
setCode(newCode);
|
||||
|
||||
if (text.length > 0 && index < OTP_LENGTH - 1) {
|
||||
inputRefs.current[index + 1]?.focus();
|
||||
}
|
||||
if (text.length === 0 && index > 0) {
|
||||
inputRefs.current[index - 1]?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyPress = ({ nativeEvent }: any, index: number) => {
|
||||
if (nativeEvent.key === 'Backspace' && code[index] === '' && index > 0) {
|
||||
inputRefs.current[index - 1]?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const handleResendCode = () => {
|
||||
resendMutate({
|
||||
phoneNumber: phoneNumber,
|
||||
otpType: 'LOGIN',
|
||||
});
|
||||
};
|
||||
|
||||
const handleVerifyCode = () => {
|
||||
const enteredCode = code.join('');
|
||||
mutate({
|
||||
phoneNumber: phoneNumber,
|
||||
otp: String(enteredCode),
|
||||
otpType: 'LOGIN',
|
||||
});
|
||||
// navigation.navigate('Home');
|
||||
};
|
||||
|
||||
return (
|
||||
<ImageBackground
|
||||
source={Logo}
|
||||
style={Loginstyle.background}
|
||||
resizeMode="contain"
|
||||
imageStyle={{
|
||||
opacity: 0.3,
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
transform: [{ scale: 1.5 }],
|
||||
}}
|
||||
>
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<View style={styles.langContainer}>
|
||||
<TouchableOpacity onPress={() => navigation.navigate('Login')}>
|
||||
<SimpleLineIcons name="arrow-left" color="#000" size={20} />
|
||||
</TouchableOpacity>
|
||||
<LanguageSelector />
|
||||
</View>
|
||||
<KeyboardAvoidingView
|
||||
style={styles.container}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
>
|
||||
<View style={styles.content}>
|
||||
<Text style={styles.title}>{t('Tasdiqlash kodini kiriting')}</Text>
|
||||
<Text style={styles.message}>
|
||||
{phoneNumber} {t('raqamiga yuborilgan')} {OTP_LENGTH}{' '}
|
||||
{t('xonali kodni kiriting.')}
|
||||
</Text>
|
||||
<View style={styles.otpContainer}>
|
||||
{code.map((digit, index) => (
|
||||
<TextInput
|
||||
key={index}
|
||||
ref={ref => {
|
||||
inputRefs.current[index] = ref;
|
||||
}}
|
||||
style={styles.otpInput}
|
||||
keyboardType="number-pad"
|
||||
maxLength={1}
|
||||
onChangeText={text => handleCodeChange(text, index)}
|
||||
onKeyPress={e => handleKeyPress(e, index)}
|
||||
value={digit}
|
||||
autoFocus={index === 0}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
{errorConfirm !== null && (
|
||||
<Text style={styles.errorText}>{errorConfirm}</Text>
|
||||
)}
|
||||
<View style={styles.resendContainer}>
|
||||
{canResend ? (
|
||||
<TouchableOpacity
|
||||
onPress={handleResendCode}
|
||||
style={styles.resendButton}
|
||||
>
|
||||
<Text style={styles.resendButtonText}>
|
||||
{t('Kodni qayta yuborish')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<Text style={styles.timerText}>
|
||||
{t('Kodni qayta yuborish vaqti')} ({timer}s)
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={styles.verifyButton}
|
||||
onPress={handleVerifyCode}
|
||||
>
|
||||
{isPending ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<Text
|
||||
style={[
|
||||
Loginstyle.btnText,
|
||||
isPending && styles.buttonTextDisabled,
|
||||
]}
|
||||
>
|
||||
{t('Tasdiqlash')}
|
||||
</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
</ImageBackground>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safeArea: {
|
||||
flex: 1,
|
||||
backgroundColor: '#f8f9fa',
|
||||
},
|
||||
errorText: {
|
||||
color: 'red',
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
marginTop: 10,
|
||||
textAlign: 'center',
|
||||
},
|
||||
buttonTextDisabled: {
|
||||
color: 'white',
|
||||
},
|
||||
langContainer: {
|
||||
width: '100%',
|
||||
marginTop: 10,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
color: '#333',
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 20,
|
||||
},
|
||||
content: {
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
color: '#333',
|
||||
marginBottom: 10,
|
||||
textAlign: 'center',
|
||||
},
|
||||
message: {
|
||||
fontSize: 15,
|
||||
color: '#666',
|
||||
textAlign: 'center',
|
||||
marginBottom: 30,
|
||||
lineHeight: 22,
|
||||
},
|
||||
otpContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
},
|
||||
otpInput: {
|
||||
width: 45,
|
||||
height: 55,
|
||||
borderWidth: 1,
|
||||
borderColor: '#ccc',
|
||||
borderRadius: 8,
|
||||
textAlign: 'center',
|
||||
fontSize: 22,
|
||||
fontWeight: 'bold',
|
||||
color: '#333',
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
resendContainer: {
|
||||
marginBottom: 30,
|
||||
marginTop: 20,
|
||||
},
|
||||
timerText: {
|
||||
fontSize: 15,
|
||||
color: '#999',
|
||||
},
|
||||
resendButton: {
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 20,
|
||||
borderRadius: 8,
|
||||
},
|
||||
resendButtonText: {
|
||||
fontSize: 15,
|
||||
color: '#007bff',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
verifyButton: {
|
||||
backgroundColor: '#007bff',
|
||||
paddingVertical: 15,
|
||||
paddingHorizontal: 40,
|
||||
borderRadius: 8,
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
elevation: 3,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 4,
|
||||
},
|
||||
verifyButtonText: {
|
||||
color: '#fff',
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
||||
export default Confirm;
|
||||
327
src/screens/auth/login/ui/index.tsx
Normal file
327
src/screens/auth/login/ui/index.tsx
Normal file
@@ -0,0 +1,327 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { authApi } from 'api/auth';
|
||||
import { loginPayload } from 'api/auth/type';
|
||||
import { Branch, branchApi } from 'api/branch';
|
||||
import formatPhone from 'helpers/formatPhone';
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
ImageBackground,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
ScrollView,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import SimpleLineIcons from 'react-native-vector-icons/SimpleLineIcons';
|
||||
import Logo from 'screens/../../assets/bootsplash/logo_512.png';
|
||||
import { LoginFormType, loginSchema } from 'screens/auth/login/lib/form';
|
||||
import LanguageSelector from 'screens/auth/select-language/SelectLang';
|
||||
import { useUserStore } from '../lib/userstore';
|
||||
import { Loginstyle } from './styled';
|
||||
|
||||
const Filial = [
|
||||
{ label: 'Toshkent shahar' },
|
||||
{ label: 'Andijon viloyati' },
|
||||
{ label: 'Samarqand viloyati' },
|
||||
{ label: 'Toshkent viloyati' },
|
||||
{ label: 'Xorazm viloyati' },
|
||||
];
|
||||
|
||||
const Login = () => {
|
||||
const { t } = useTranslation();
|
||||
const passportNumberRef = useRef<TextInput>(null);
|
||||
const [filialDropdownVisible, setFilialDropdownVisible] = useState(false);
|
||||
const navigation = useNavigation<NativeStackNavigationProp<any>>();
|
||||
const { setUser, setExpireTime } = useUserStore(state => state);
|
||||
const [error, setError] = useState<string>();
|
||||
const [rawPhone, setRawPhone] = useState('+998');
|
||||
const { data: branchList } = useQuery({
|
||||
queryKey: ['branchList'],
|
||||
queryFn: branchApi.branchList,
|
||||
});
|
||||
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: (payload: loginPayload) => authApi.login(payload),
|
||||
onSuccess: res => {
|
||||
navigation.navigate('Login-Confirm');
|
||||
setExpireTime(res.data.expireTime);
|
||||
},
|
||||
onError: err => {
|
||||
setError('Xatolik yuz berdi');
|
||||
console.dir(err);
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm<LoginFormType>({
|
||||
resolver: zodResolver(loginSchema),
|
||||
defaultValues: {
|
||||
phone: '',
|
||||
passportSeriya: '',
|
||||
passportNumber: '',
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (data: LoginFormType) => {
|
||||
mutate({
|
||||
branchId: data.branchId,
|
||||
phoneNumber: data.phone,
|
||||
passportSerial: `${data.passportSeriya.toUpperCase()}${
|
||||
data.passportNumber
|
||||
}`,
|
||||
});
|
||||
// navigation.navigate('Login-Confirm');
|
||||
setUser({
|
||||
phoneNumber: data.phone,
|
||||
});
|
||||
};
|
||||
|
||||
const handleBackNavigation = useCallback(() => {
|
||||
navigation.navigate('select-auth');
|
||||
}, [navigation]);
|
||||
|
||||
const handlePhoneChange = useCallback((text: string) => {
|
||||
const digits = text.replace(/\D/g, '');
|
||||
const full = digits.startsWith('998') ? digits : `998${digits}`;
|
||||
setRawPhone(`+${full}`);
|
||||
setValue('phone', full, { shouldValidate: true });
|
||||
}, []);
|
||||
|
||||
const keyboardBehavior = useMemo(
|
||||
() => (Platform.OS === 'ios' ? 'padding' : 'height'),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<ImageBackground
|
||||
source={Logo}
|
||||
style={Loginstyle.background}
|
||||
resizeMode="contain"
|
||||
imageStyle={{
|
||||
opacity: 0.3,
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
transform: [{ scale: 1.5 }],
|
||||
}}
|
||||
>
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<View style={Loginstyle.langContainer}>
|
||||
<TouchableOpacity onPress={handleBackNavigation}>
|
||||
<SimpleLineIcons name="arrow-left" color="#000" size={20} />
|
||||
</TouchableOpacity>
|
||||
<LanguageSelector />
|
||||
</View>
|
||||
<KeyboardAvoidingView
|
||||
style={Loginstyle.container}
|
||||
behavior={keyboardBehavior}
|
||||
>
|
||||
<ScrollView style={{ flex: 1 }}>
|
||||
<View style={Loginstyle.scrollContainer}>
|
||||
<View style={Loginstyle.loginContainer}>
|
||||
<Text style={Loginstyle.title}>{t('Tizimga kirish')}</Text>
|
||||
<Controller
|
||||
control={control}
|
||||
name="phone"
|
||||
render={({ field: { onChange } }) => {
|
||||
const formatted = formatPhone(rawPhone);
|
||||
return (
|
||||
<View>
|
||||
<Text style={Loginstyle.label}>
|
||||
{t('Telefon raqami')}
|
||||
</Text>
|
||||
<TextInput
|
||||
keyboardType="numeric"
|
||||
placeholder="+998 90 123-45-67"
|
||||
value={formatted}
|
||||
onChangeText={handlePhoneChange}
|
||||
style={Loginstyle.input}
|
||||
placeholderTextColor="#D8DADC"
|
||||
maxLength={19} // +998 90 123-45-67 bo'lishi uchun
|
||||
/>
|
||||
{errors.phone && (
|
||||
<Text style={Loginstyle.errorText}>
|
||||
{t(errors.phone.message || '')}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<View>
|
||||
<Text style={Loginstyle.label}>
|
||||
{t('Passport seriya raqami')}
|
||||
</Text>
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="passportSeriya"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextInput
|
||||
style={[Loginstyle.input, Loginstyle.seriyaInput]}
|
||||
placeholder="AA"
|
||||
maxLength={2}
|
||||
autoCapitalize="characters"
|
||||
value={value}
|
||||
onChangeText={text => {
|
||||
onChange(text);
|
||||
if (text.length === 2) {
|
||||
passportNumberRef.current?.focus();
|
||||
}
|
||||
}}
|
||||
placeholderTextColor="#D8DADC"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="passportNumber"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextInput
|
||||
ref={passportNumberRef}
|
||||
style={[Loginstyle.input, Loginstyle.raqamInput]}
|
||||
placeholder="1234567"
|
||||
maxLength={7}
|
||||
keyboardType="numeric"
|
||||
value={value}
|
||||
onChangeText={text => {
|
||||
const onlyNumbers = text.replace(/[^0-9]/g, '');
|
||||
onChange(onlyNumbers);
|
||||
}}
|
||||
placeholderTextColor="#D8DADC"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
{(errors.passportSeriya || errors.passportNumber) && (
|
||||
<Text style={Loginstyle.errorText}>
|
||||
{t(errors.passportSeriya?.message || '') ||
|
||||
t(errors.passportNumber?.message || '')}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="branchId"
|
||||
render={({ field: { value } }) => (
|
||||
<View style={{ position: 'relative' }}>
|
||||
<Text style={Loginstyle.label}>{t('Filial')}</Text>
|
||||
<View style={Loginstyle.input}>
|
||||
<TouchableOpacity
|
||||
style={Loginstyle.selector}
|
||||
onPress={() =>
|
||||
setFilialDropdownVisible(prev => !prev)
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
value
|
||||
? { color: '#000' }
|
||||
: Loginstyle.selectedText
|
||||
}
|
||||
>
|
||||
{branchList?.find(e => e.id === value)?.name ||
|
||||
t('Filialni tanlang...')}
|
||||
</Text>
|
||||
<SimpleLineIcons
|
||||
name={
|
||||
filialDropdownVisible ? 'arrow-up' : 'arrow-down'
|
||||
}
|
||||
color="#000"
|
||||
size={14}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
{filialDropdownVisible && (
|
||||
<View style={[Loginstyle.dropdown, { maxHeight: 200 }]}>
|
||||
<ScrollView nestedScrollEnabled>
|
||||
{branchList &&
|
||||
branchList.map((item: Branch) => (
|
||||
<TouchableOpacity
|
||||
key={item.id}
|
||||
style={Loginstyle.dropdownItem}
|
||||
onPress={() => {
|
||||
setValue('branchId', item.id);
|
||||
setFilialDropdownVisible(false);
|
||||
}}
|
||||
>
|
||||
<Text style={Loginstyle.dropdownItemText}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)}
|
||||
{errors.branchId && (
|
||||
<Text style={Loginstyle.errorText}>
|
||||
{t(errors.branchId.message || '')}
|
||||
</Text>
|
||||
)}
|
||||
{error && (
|
||||
<Text style={[Loginstyle.errorText]}>{t(error)}</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={handleSubmit(onSubmit)}
|
||||
style={Loginstyle.button}
|
||||
>
|
||||
{isPending ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<Text style={Loginstyle.btnText}>
|
||||
{t('Tizimga kirish')}
|
||||
</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
<View
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: 14, fontWeight: '500' }}>
|
||||
{t('ID va kabinet yo’qmi?')}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={Loginstyle.dropdownItem}
|
||||
onPress={() => navigation.navigate('Register')}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: '#28A7E8',
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
}}
|
||||
>
|
||||
{t('Ro’yxatdan o’tish')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
</ImageBackground>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
126
src/screens/auth/login/ui/styled.ts
Normal file
126
src/screens/auth/login/ui/styled.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
export const Loginstyle = StyleSheet.create({
|
||||
langContainer: {
|
||||
width: '100%',
|
||||
marginTop: 10,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
background: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
overlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.7)',
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContainer: {
|
||||
flexGrow: 1,
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
},
|
||||
loginContainer: {
|
||||
borderRadius: 20,
|
||||
padding: 30,
|
||||
display: 'flex',
|
||||
gap: 20,
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
},
|
||||
label: {
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
color: '#28A7E8',
|
||||
marginBottom: 5,
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
color: '#28A7E8',
|
||||
marginBottom: 20,
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#D8DADC',
|
||||
borderRadius: 12,
|
||||
padding: 15,
|
||||
fontSize: 16,
|
||||
fontWeight: '400',
|
||||
backgroundColor: '#FFFFFF',
|
||||
color: '#000',
|
||||
},
|
||||
seriyaInput: {
|
||||
width: 60,
|
||||
fontSize: 14,
|
||||
textTransform: 'uppercase',
|
||||
borderTopRightRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
},
|
||||
raqamInput: {
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
},
|
||||
selector: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
},
|
||||
selectedText: {
|
||||
fontSize: 14,
|
||||
color: '#D8DADC',
|
||||
},
|
||||
dropdown: {
|
||||
position: 'absolute',
|
||||
top: 80,
|
||||
left: 0,
|
||||
right: 0,
|
||||
backgroundColor: '#fff',
|
||||
borderWidth: 1,
|
||||
borderColor: '#ccc',
|
||||
borderRadius: 8,
|
||||
zIndex: 10,
|
||||
elevation: 5,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 4,
|
||||
maxHeight: 150,
|
||||
},
|
||||
dropdownItem: {
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 12,
|
||||
},
|
||||
dropdownItemText: {
|
||||
fontSize: 14,
|
||||
color: '#333',
|
||||
},
|
||||
button: {
|
||||
backgroundColor: '#28A7E8',
|
||||
height: 56,
|
||||
borderRadius: 8,
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
btnText: {
|
||||
color: '#fff',
|
||||
fontSize: 20,
|
||||
textAlign: 'center',
|
||||
},
|
||||
errorText: {
|
||||
color: 'red',
|
||||
fontSize: 12,
|
||||
},
|
||||
});
|
||||
20
src/screens/auth/registeration/lib/form.ts
Normal file
20
src/screens/auth/registeration/lib/form.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// form.ts
|
||||
import { z } from 'zod';
|
||||
|
||||
export const FirstStepSchema = z.object({
|
||||
firstName: z.string().min(3, "Eng kamida 3ta belgi bo'lishi kerak"),
|
||||
lastName: z.string().min(3, "Eng kamida 3ta belgi bo'lishi kerak"),
|
||||
phoneNumber: z.string().min(12, 'Xato raqam kiritildi'),
|
||||
branchId: z.number().min(1, 'Filialni tanlang'),
|
||||
recommend: z.string().min(1, 'Majburiy maydon'),
|
||||
});
|
||||
|
||||
export const SecondStepSchema = z.object({
|
||||
passportSeriya: z.string().length(2, '2 ta harf kerak'),
|
||||
birthDate: z.string().min(8, 'Majburiy maydon'),
|
||||
passportNumber: z.string().length(7, '7 ta raqam kerak'),
|
||||
jshshir: z.string().length(14, '14 ta raqam kerak'),
|
||||
});
|
||||
|
||||
export type FirstStepFormType = z.infer<typeof FirstStepSchema>;
|
||||
export type SecondStepFormType = z.infer<typeof SecondStepSchema>;
|
||||
42
src/screens/auth/registeration/lib/modalStore.ts
Normal file
42
src/screens/auth/registeration/lib/modalStore.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface ModalState {
|
||||
isVisible: boolean;
|
||||
title: string;
|
||||
message: string;
|
||||
type: 'success' | 'error' | 'info';
|
||||
onConfirm: () => void;
|
||||
onCancel?: () => void;
|
||||
openModal: (
|
||||
title: string,
|
||||
message: string,
|
||||
type?: 'success' | 'error' | 'info',
|
||||
onConfirm?: () => void,
|
||||
onCancel?: () => void
|
||||
) => void;
|
||||
closeModal: () => void;
|
||||
}
|
||||
|
||||
export const useModalStore = create<ModalState>((set) => ({
|
||||
isVisible: false,
|
||||
title: '',
|
||||
message: '',
|
||||
type: 'info',
|
||||
onConfirm: () => { },
|
||||
onCancel: undefined,
|
||||
|
||||
openModal: (title, message, type = 'info', onConfirm, onCancel) =>
|
||||
set({
|
||||
isVisible: true,
|
||||
title,
|
||||
message,
|
||||
type,
|
||||
onConfirm: () => {
|
||||
if (onConfirm) onConfirm();
|
||||
set({ isVisible: false });
|
||||
},
|
||||
onCancel: onCancel || undefined,
|
||||
}),
|
||||
|
||||
closeModal: () => set({ isVisible: false }),
|
||||
}));
|
||||
28
src/screens/auth/registeration/lib/userstore.ts
Normal file
28
src/screens/auth/registeration/lib/userstore.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface UserState {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
phoneNumber: string;
|
||||
setUser: (user: Partial<UserState>) => void;
|
||||
clearUser: () => void;
|
||||
}
|
||||
|
||||
export const useUserStore = create<UserState>(set => ({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
phoneNumber: '',
|
||||
|
||||
setUser: user =>
|
||||
set(state => ({
|
||||
...state,
|
||||
...user,
|
||||
})),
|
||||
|
||||
clearUser: () =>
|
||||
set({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
phoneNumber: '',
|
||||
}),
|
||||
}));
|
||||
320
src/screens/auth/registeration/ui/Confirm.tsx
Normal file
320
src/screens/auth/registeration/ui/Confirm.tsx
Normal file
@@ -0,0 +1,320 @@
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { authApi } from 'api/auth';
|
||||
import { otpPayload, resendPayload } from 'api/auth/type';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
ImageBackground,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import SimpleLineIcons from 'react-native-vector-icons/SimpleLineIcons';
|
||||
import Logo from 'screens/../../assets/bootsplash/logo_512.png';
|
||||
import { useModalStore } from 'screens/auth/registeration/lib/modalStore';
|
||||
import LanguageSelector from 'screens/auth/select-language/SelectLang';
|
||||
import { RootStackParamList } from 'types/types';
|
||||
import { useUserStore } from '../lib/userstore';
|
||||
import { RegisterStyle } from './styled';
|
||||
|
||||
type VerificationCodeScreenNavigationProp = NativeStackNavigationProp<
|
||||
RootStackParamList,
|
||||
'Confirm'
|
||||
>;
|
||||
|
||||
const OTP_LENGTH = 4;
|
||||
|
||||
const Confirm = ({
|
||||
setStep,
|
||||
}: {
|
||||
setStep: React.Dispatch<React.SetStateAction<number>>;
|
||||
}) => {
|
||||
const navigation = useNavigation<VerificationCodeScreenNavigationProp>();
|
||||
const { t } = useTranslation();
|
||||
const [code, setCode] = useState<string[]>(new Array(OTP_LENGTH).fill(''));
|
||||
const [timer, setTimer] = useState(60);
|
||||
const [canResend, setCanResend] = useState(false);
|
||||
const [errorConfirm, setErrorConfirm] = useState<string | null>(null);
|
||||
const inputRefs = useRef<Array<TextInput | null>>([]);
|
||||
const { phoneNumber } = useUserStore(state => state);
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: (payload: otpPayload) => authApi.verifyOtp(payload),
|
||||
onSuccess: async res => {
|
||||
await AsyncStorage.setItem('token', res.data.accessToken);
|
||||
navigation.navigate('Confirm');
|
||||
setErrorConfirm(null);
|
||||
},
|
||||
onError: (err: any) => {
|
||||
setErrorConfirm(err?.response.data.message);
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: resendMutate } = useMutation({
|
||||
mutationFn: (payload: resendPayload) => authApi.resendOtp(payload),
|
||||
onSuccess: async res => {
|
||||
setTimer(60);
|
||||
setCanResend(false);
|
||||
setCode(new Array(OTP_LENGTH).fill(''));
|
||||
inputRefs.current[0]?.focus();
|
||||
setErrorConfirm(null);
|
||||
},
|
||||
onError: (err: any) => {
|
||||
setErrorConfirm(err?.response.data.message);
|
||||
},
|
||||
});
|
||||
|
||||
const openModal = useModalStore(state => state.openModal);
|
||||
useEffect(() => {
|
||||
let interval: NodeJS.Timeout | null = null;
|
||||
if (timer > 0) {
|
||||
interval = setInterval(() => {
|
||||
setTimer(prevTimer => prevTimer - 1);
|
||||
}, 1000);
|
||||
} else {
|
||||
setCanResend(true);
|
||||
if (interval) clearInterval(interval);
|
||||
}
|
||||
return () => {
|
||||
if (interval) clearInterval(interval);
|
||||
};
|
||||
}, [timer]);
|
||||
|
||||
const handleCodeChange = (text: string, index: number) => {
|
||||
const newCode = [...code];
|
||||
newCode[index] = text;
|
||||
setCode(newCode);
|
||||
|
||||
if (text.length > 0 && index < OTP_LENGTH - 1) {
|
||||
inputRefs.current[index + 1]?.focus();
|
||||
}
|
||||
if (text.length === 0 && index > 0) {
|
||||
inputRefs.current[index - 1]?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyPress = ({ nativeEvent }: any, index: number) => {
|
||||
if (nativeEvent.key === 'Backspace' && code[index] === '' && index > 0) {
|
||||
inputRefs.current[index - 1]?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const handleResendCode = () => {
|
||||
resendMutate({
|
||||
phoneNumber: phoneNumber,
|
||||
otpType: 'REGISTRATION',
|
||||
});
|
||||
};
|
||||
|
||||
const handleVerifyCode = () => {
|
||||
const enteredCode = code.join('');
|
||||
mutate({
|
||||
phoneNumber: phoneNumber,
|
||||
otp: String(enteredCode),
|
||||
otpType: 'REGISTRATION',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ImageBackground
|
||||
source={Logo}
|
||||
style={RegisterStyle.background}
|
||||
resizeMode="contain"
|
||||
imageStyle={{
|
||||
opacity: 0.3,
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
transform: [{ scale: 1.5 }],
|
||||
}}
|
||||
>
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<View style={styles.langContainer}>
|
||||
<TouchableOpacity onPress={() => setStep(1)}>
|
||||
<SimpleLineIcons name="arrow-left" color="#000" size={20} />
|
||||
</TouchableOpacity>
|
||||
<LanguageSelector />
|
||||
</View>
|
||||
<KeyboardAvoidingView
|
||||
style={styles.container}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
>
|
||||
<View style={styles.content}>
|
||||
<Text style={styles.title}>{t('Tasdiqlash kodini kiriting')}</Text>
|
||||
<Text style={styles.message}>
|
||||
{phoneNumber} {t('raqamiga yuborilgan')} {OTP_LENGTH}{' '}
|
||||
{t('xonali kodni kiriting.')}
|
||||
</Text>
|
||||
<View style={styles.otpContainer}>
|
||||
{code.map((digit, index) => (
|
||||
<TextInput
|
||||
key={index}
|
||||
ref={ref => {
|
||||
inputRefs.current[index] = ref;
|
||||
}}
|
||||
style={styles.otpInput}
|
||||
keyboardType="number-pad"
|
||||
maxLength={1}
|
||||
onChangeText={text => handleCodeChange(text, index)}
|
||||
onKeyPress={e => handleKeyPress(e, index)}
|
||||
value={digit}
|
||||
autoFocus={index === 0}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
{errorConfirm !== null && (
|
||||
<Text style={styles.errorText}>{errorConfirm}</Text>
|
||||
)}
|
||||
<View style={styles.resendContainer}>
|
||||
{canResend ? (
|
||||
<TouchableOpacity
|
||||
onPress={handleResendCode}
|
||||
style={styles.resendButton}
|
||||
>
|
||||
<Text style={styles.resendButtonText}>
|
||||
{t('Kodni qayta yuborish')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<Text style={styles.timerText}>
|
||||
{t('Kodni qayta yuborish vaqti')} ({timer}s)
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={styles.verifyButton}
|
||||
onPress={handleVerifyCode}
|
||||
>
|
||||
{isPending ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<Text
|
||||
style={[
|
||||
RegisterStyle.btnText,
|
||||
isPending && RegisterStyle.buttonTextDisabled,
|
||||
]}
|
||||
>
|
||||
{t('Tasdiqlash')}
|
||||
</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
</ImageBackground>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safeArea: {
|
||||
flex: 1,
|
||||
backgroundColor: '#f8f9fa',
|
||||
},
|
||||
errorText: {
|
||||
color: 'red',
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
marginTop: 10,
|
||||
textAlign: 'center',
|
||||
},
|
||||
langContainer: {
|
||||
width: '100%',
|
||||
marginTop: 10,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
color: '#333',
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 20,
|
||||
},
|
||||
content: {
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
color: '#333',
|
||||
marginBottom: 10,
|
||||
textAlign: 'center',
|
||||
},
|
||||
message: {
|
||||
fontSize: 15,
|
||||
color: '#666',
|
||||
textAlign: 'center',
|
||||
marginBottom: 30,
|
||||
lineHeight: 22,
|
||||
},
|
||||
otpContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
},
|
||||
otpInput: {
|
||||
width: 45,
|
||||
height: 55,
|
||||
borderWidth: 1,
|
||||
borderColor: '#ccc',
|
||||
borderRadius: 8,
|
||||
textAlign: 'center',
|
||||
fontSize: 22,
|
||||
fontWeight: 'bold',
|
||||
color: '#333',
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
resendContainer: {
|
||||
marginBottom: 30,
|
||||
},
|
||||
timerText: {
|
||||
fontSize: 15,
|
||||
color: '#999',
|
||||
},
|
||||
resendButton: {
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 20,
|
||||
borderRadius: 8,
|
||||
},
|
||||
resendButtonText: {
|
||||
fontSize: 15,
|
||||
color: '#007bff',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
verifyButton: {
|
||||
backgroundColor: '#007bff',
|
||||
paddingVertical: 15,
|
||||
paddingHorizontal: 40,
|
||||
borderRadius: 8,
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
elevation: 3,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 4,
|
||||
},
|
||||
verifyButtonText: {
|
||||
color: '#fff',
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
||||
export default Confirm;
|
||||
468
src/screens/auth/registeration/ui/FirstStep.tsx
Normal file
468
src/screens/auth/registeration/ui/FirstStep.tsx
Normal file
@@ -0,0 +1,468 @@
|
||||
'use client';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import {
|
||||
type RouteProp,
|
||||
useNavigation,
|
||||
useRoute,
|
||||
} from '@react-navigation/native';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { authApi } from 'api/auth';
|
||||
import { registerPayload } from 'api/auth/type';
|
||||
import { Branch, branchApi } from 'api/branch';
|
||||
import formatPhone from 'helpers/formatPhone';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Animated,
|
||||
ImageBackground,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
ScrollView,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import AntDesign from 'react-native-vector-icons/AntDesign';
|
||||
import SimpleLineIcons from 'react-native-vector-icons/SimpleLineIcons';
|
||||
import Logo from 'screens/../../assets/bootsplash/logo_512.png';
|
||||
import {
|
||||
FirstStepFormType,
|
||||
FirstStepSchema,
|
||||
} from 'screens/auth/registeration/lib/form';
|
||||
import LanguageSelector from 'screens/auth/select-language/SelectLang';
|
||||
import { RootStackParamList } from 'types/types';
|
||||
import { useUserStore } from '../lib/userstore';
|
||||
import { RegisterStyle } from './styled';
|
||||
|
||||
type LoginScreenNavigationProp = NativeStackNavigationProp<
|
||||
RootStackParamList,
|
||||
'Login'
|
||||
>;
|
||||
|
||||
const recommended = [
|
||||
{ label: 'Tanishim orqali', value: 'FRIEND' },
|
||||
{ label: 'Telegram orqali', value: 'TELEGRAM' },
|
||||
{ label: 'Instagram orqali', value: 'INSTAGRAM' },
|
||||
{ label: 'Facebook orqali', value: 'FACEBOOK' },
|
||||
];
|
||||
|
||||
const FirstStep = ({ onNext }: { onNext: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const [filialDropdownVisible, setFilialDropdownVisible] = useState(false);
|
||||
const [error, setError] = useState<string>();
|
||||
const { setUser } = useUserStore(state => state);
|
||||
const { data: branchList } = useQuery({
|
||||
queryKey: ['branchList'],
|
||||
queryFn: branchApi.branchList,
|
||||
});
|
||||
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: (payload: registerPayload) => authApi.register(payload),
|
||||
onSuccess: res => {
|
||||
onNext();
|
||||
},
|
||||
onError: err => {
|
||||
console.dir(err);
|
||||
|
||||
setError('Xatolik yuz berdi');
|
||||
},
|
||||
});
|
||||
|
||||
const [recommendedDropdownVisible, setRecommendedDropdownVisible] =
|
||||
useState(false);
|
||||
const [termsAccepted, setTermsAccepted] = useState(false);
|
||||
const [checkboxAnimation] = useState(new Animated.Value(0));
|
||||
const navigation = useNavigation<LoginScreenNavigationProp>();
|
||||
const [rawPhone, setRawPhone] = useState('+998');
|
||||
const route = useRoute<RouteProp<RootStackParamList, 'Register'>>();
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
getValues,
|
||||
} = useForm<FirstStepFormType>({
|
||||
resolver: zodResolver(FirstStepSchema),
|
||||
defaultValues: {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
recommend: '',
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (data: FirstStepFormType) => {
|
||||
setUser({
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
phoneNumber: data.phoneNumber,
|
||||
});
|
||||
mutate(data);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (route.params?.termsAccepted) {
|
||||
setTermsAccepted(true);
|
||||
Animated.spring(checkboxAnimation, {
|
||||
toValue: 1,
|
||||
useNativeDriver: false,
|
||||
tension: 100,
|
||||
friction: 8,
|
||||
}).start();
|
||||
}
|
||||
}, [route.params]);
|
||||
|
||||
const navigateToTerms = () => {
|
||||
navigation.navigate('TermsAndConditions');
|
||||
setTermsAccepted(true);
|
||||
Animated.spring(checkboxAnimation, {
|
||||
toValue: 1,
|
||||
useNativeDriver: false,
|
||||
tension: 100,
|
||||
friction: 8,
|
||||
}).start();
|
||||
};
|
||||
|
||||
const toggleCheckbox = () => {
|
||||
if (!termsAccepted) {
|
||||
navigateToTerms();
|
||||
} else {
|
||||
setTermsAccepted(false);
|
||||
Animated.spring(checkboxAnimation, {
|
||||
toValue: 0,
|
||||
useNativeDriver: false,
|
||||
tension: 100,
|
||||
friction: 8,
|
||||
}).start();
|
||||
}
|
||||
};
|
||||
return (
|
||||
<ImageBackground
|
||||
source={Logo}
|
||||
style={RegisterStyle.background}
|
||||
resizeMode="contain"
|
||||
imageStyle={{
|
||||
opacity: 0.3,
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
transform: [{ scale: 1.5 }],
|
||||
}}
|
||||
>
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<View style={RegisterStyle.langContainer}>
|
||||
<TouchableOpacity onPress={() => navigation.navigate('select-auth')}>
|
||||
<SimpleLineIcons name="arrow-left" color="#000" size={20} />
|
||||
</TouchableOpacity>
|
||||
<LanguageSelector />
|
||||
</View>
|
||||
<KeyboardAvoidingView
|
||||
style={RegisterStyle.container}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
>
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
style={RegisterStyle.content}
|
||||
>
|
||||
<View style={RegisterStyle.scrollContainer}>
|
||||
<View style={RegisterStyle.loginContainer}>
|
||||
<Text style={RegisterStyle.title}>
|
||||
{t("Ro'yxatdan o'tish")}
|
||||
</Text>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="firstName"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<View>
|
||||
<Text style={RegisterStyle.label}>{t('Ism')}</Text>
|
||||
<TextInput
|
||||
style={RegisterStyle.input}
|
||||
placeholder={t('Ismingiz')}
|
||||
onChangeText={onChange}
|
||||
value={value}
|
||||
placeholderTextColor={'#D8DADC'}
|
||||
/>
|
||||
{errors.firstName && (
|
||||
<Text style={RegisterStyle.errorText}>
|
||||
{t(errors.firstName.message || '')}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="lastName"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<View>
|
||||
<Text style={RegisterStyle.label}>{t('Familiya')}</Text>
|
||||
<TextInput
|
||||
style={RegisterStyle.input}
|
||||
placeholder={t('Familiyangiz')}
|
||||
placeholderTextColor={'#D8DADC'}
|
||||
onChangeText={onChange}
|
||||
value={value}
|
||||
/>
|
||||
{errors.lastName && (
|
||||
<Text style={RegisterStyle.errorText}>
|
||||
{t(errors.lastName.message || '')}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="phoneNumber"
|
||||
render={({ field: { onChange } }) => {
|
||||
const formatted = formatPhone(rawPhone);
|
||||
return (
|
||||
<View>
|
||||
<Text style={RegisterStyle.label}>
|
||||
{t('Telefon raqami')}
|
||||
</Text>
|
||||
<TextInput
|
||||
keyboardType="numeric"
|
||||
placeholder="+998 __ ___-__-__"
|
||||
value={formatted}
|
||||
onChangeText={text => {
|
||||
const digits = text.replace(/\D/g, '').slice(0, 12);
|
||||
const full = digits.startsWith('998')
|
||||
? digits
|
||||
: `998${digits}`;
|
||||
setRawPhone(full);
|
||||
onChange(full);
|
||||
}}
|
||||
style={RegisterStyle.input}
|
||||
placeholderTextColor="#D8DADC"
|
||||
maxLength={17}
|
||||
/>
|
||||
{errors.phoneNumber && (
|
||||
<Text style={RegisterStyle.errorText}>
|
||||
{t(errors.phoneNumber.message || '')}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="branchId"
|
||||
render={({ field: { value } }) => (
|
||||
<View style={{ position: 'relative' }}>
|
||||
<Text style={RegisterStyle.label}>{t('Filial')}</Text>
|
||||
<View style={RegisterStyle.input}>
|
||||
<TouchableOpacity
|
||||
style={RegisterStyle.selector}
|
||||
onPress={() =>
|
||||
setFilialDropdownVisible(prev => !prev)
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
value
|
||||
? { color: '#000' }
|
||||
: RegisterStyle.selectedText
|
||||
}
|
||||
>
|
||||
{branchList?.find(e => e.id === value)?.name ||
|
||||
t('Filialni tanlang...')}
|
||||
</Text>
|
||||
<SimpleLineIcons
|
||||
name={
|
||||
filialDropdownVisible ? 'arrow-up' : 'arrow-down'
|
||||
}
|
||||
color="#000"
|
||||
size={14}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
{filialDropdownVisible && (
|
||||
<View
|
||||
style={[RegisterStyle.dropdown, { maxHeight: 200 }]}
|
||||
>
|
||||
<ScrollView nestedScrollEnabled>
|
||||
{branchList &&
|
||||
branchList.map((item: Branch) => (
|
||||
<TouchableOpacity
|
||||
key={item.id}
|
||||
style={RegisterStyle.dropdownItem}
|
||||
onPress={() => {
|
||||
setValue('branchId', item.id);
|
||||
setFilialDropdownVisible(false);
|
||||
}}
|
||||
>
|
||||
<Text style={RegisterStyle.dropdownItemText}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)}
|
||||
{errors.branchId && (
|
||||
<Text style={RegisterStyle.errorText}>
|
||||
{t(errors.branchId.message || '')}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="recommend"
|
||||
render={({ field: { value } }) => (
|
||||
<View style={{ position: 'relative' }}>
|
||||
<Text style={RegisterStyle.label}>
|
||||
{t('Bizni qaerdan topdingiz?')}
|
||||
</Text>
|
||||
<View style={RegisterStyle.input}>
|
||||
<TouchableOpacity
|
||||
style={RegisterStyle.selector}
|
||||
onPress={() =>
|
||||
setRecommendedDropdownVisible(prev => !prev)
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
value
|
||||
? { color: '#000' }
|
||||
: RegisterStyle.selectedText
|
||||
}
|
||||
>
|
||||
{t(
|
||||
recommended.find(e => e.value === value)?.label ||
|
||||
'Bizni kim tavsiya qildi...',
|
||||
)}
|
||||
</Text>
|
||||
<SimpleLineIcons
|
||||
name={
|
||||
recommendedDropdownVisible
|
||||
? 'arrow-up'
|
||||
: 'arrow-down'
|
||||
}
|
||||
color="#000"
|
||||
size={14}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
{recommendedDropdownVisible && (
|
||||
<View
|
||||
style={[RegisterStyle.dropdown, { maxHeight: 200 }]}
|
||||
>
|
||||
<ScrollView nestedScrollEnabled>
|
||||
{recommended.map((item, index) => (
|
||||
<TouchableOpacity
|
||||
key={index}
|
||||
style={RegisterStyle.dropdownItem}
|
||||
onPress={() => {
|
||||
setValue('recommend', item.value);
|
||||
setRecommendedDropdownVisible(false);
|
||||
}}
|
||||
>
|
||||
<Text style={RegisterStyle.dropdownItemText}>
|
||||
{t(item.label)}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)}
|
||||
{errors.recommend && (
|
||||
<Text style={RegisterStyle.errorText}>
|
||||
{t(errors.recommend.message || '')}
|
||||
</Text>
|
||||
)}
|
||||
{error && (
|
||||
<Text style={[RegisterStyle.errorText]}>
|
||||
{t(error)}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
<View style={RegisterStyle.termsContainer}>
|
||||
<TouchableOpacity
|
||||
style={RegisterStyle.checkboxContainer}
|
||||
onPress={toggleCheckbox}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Animated.View
|
||||
style={[
|
||||
RegisterStyle.checkbox,
|
||||
termsAccepted && RegisterStyle.checkboxChecked,
|
||||
{
|
||||
transform: [
|
||||
{
|
||||
scale: checkboxAnimation.interpolate({
|
||||
inputRange: [0, 0.5, 1],
|
||||
outputRange: [1, 1.1, 1],
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
>
|
||||
{termsAccepted && (
|
||||
<Animated.View
|
||||
style={{
|
||||
opacity: checkboxAnimation,
|
||||
transform: [
|
||||
{
|
||||
scale: checkboxAnimation,
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
<AntDesign name="check" color="#fff" size={20} />
|
||||
</Animated.View>
|
||||
)}
|
||||
</Animated.View>
|
||||
<View style={RegisterStyle.termsTextContainer}>
|
||||
<Text style={RegisterStyle.termsText}>
|
||||
<Text>{t('Foydalanish shartlari')}</Text>
|
||||
<Text> {t('bilan tanishib chiqdim!')}</Text>
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={handleSubmit(onSubmit)}
|
||||
style={[
|
||||
RegisterStyle.button,
|
||||
(!termsAccepted || isPending) &&
|
||||
RegisterStyle.buttonDisabled,
|
||||
]}
|
||||
disabled={!termsAccepted || isPending}
|
||||
>
|
||||
{isPending ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<Text
|
||||
style={[
|
||||
RegisterStyle.btnText,
|
||||
(!termsAccepted || isPending) &&
|
||||
RegisterStyle.buttonTextDisabled,
|
||||
]}
|
||||
>
|
||||
{t('Davom etish')}
|
||||
</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
</ImageBackground>
|
||||
);
|
||||
};
|
||||
|
||||
export default FirstStep;
|
||||
427
src/screens/auth/registeration/ui/SecondStep.tsx
Normal file
427
src/screens/auth/registeration/ui/SecondStep.tsx
Normal file
@@ -0,0 +1,427 @@
|
||||
'use client';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import {
|
||||
useNavigation,
|
||||
useRoute,
|
||||
type RouteProp,
|
||||
} from '@react-navigation/native';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import passportApi, { sendPassportPayload } from 'api/passport';
|
||||
import DatePickerInput from 'components/DatePicker';
|
||||
import SingleFileDrop from 'components/FileDrop';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Animated,
|
||||
Dimensions,
|
||||
Image,
|
||||
ImageBackground,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
ScrollView,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import AntDesign from 'react-native-vector-icons/AntDesign';
|
||||
import SimpleLineIcons from 'react-native-vector-icons/SimpleLineIcons';
|
||||
import Logo from 'screens/../../assets/bootsplash/logo_512.png';
|
||||
import LanguageSelector from 'screens/auth/select-language/SelectLang';
|
||||
import { RootStackParamList } from 'types/types';
|
||||
import { SecondStepFormType, SecondStepSchema } from '../lib/form';
|
||||
import { RegisterStyle } from './styled';
|
||||
|
||||
interface FileData {
|
||||
uri: string;
|
||||
name: string;
|
||||
type: string;
|
||||
base64: string;
|
||||
}
|
||||
|
||||
const SecondStep = () => {
|
||||
const { t } = useTranslation();
|
||||
const windowWidth = Dimensions.get('window').width;
|
||||
const [frontImage, setFrontImage] = useState<FileData | null>(null);
|
||||
const [backImage, setBackImage] = useState<FileData | null>(null);
|
||||
const isSmallScreen = windowWidth < 200;
|
||||
const [termsAccepted, setTermsAccepted] = useState(true);
|
||||
const passportNumberRef = useRef<TextInput>(null);
|
||||
const [checkboxAnimation] = useState(new Animated.Value(1));
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<RootStackParamList, 'Login'>>();
|
||||
const route = useRoute<RouteProp<RootStackParamList, 'Register'>>();
|
||||
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
|
||||
const [isDatePickerVisible, setDatePickerVisibility] = useState(false);
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const year = date.getFullYear();
|
||||
return `${day}/${month}/${year}`;
|
||||
};
|
||||
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: (payload: sendPassportPayload) =>
|
||||
passportApi.sendPassport(payload),
|
||||
onSuccess: res => {
|
||||
navigation.navigate('Home');
|
||||
},
|
||||
onError: err => {
|
||||
console.dir(err);
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (route.params?.termsAccepted) {
|
||||
setTermsAccepted(true);
|
||||
Animated.spring(checkboxAnimation, {
|
||||
toValue: 1,
|
||||
useNativeDriver: false,
|
||||
tension: 100,
|
||||
friction: 8,
|
||||
}).start();
|
||||
}
|
||||
}, [route.params]);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm<SecondStepFormType>({
|
||||
resolver: zodResolver(SecondStepSchema),
|
||||
defaultValues: {
|
||||
passportNumber: '',
|
||||
passportSeriya: '',
|
||||
birthDate: '',
|
||||
jshshir: '',
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (data: SecondStepFormType) => {
|
||||
const [d, m, y] = data.birthDate.split('/');
|
||||
const isoBirthDate = `${y}-${m.padStart(2, '0')}-${d.padStart(2, '0')}`;
|
||||
|
||||
mutate({
|
||||
fullName: data.passportSeriya.toUpperCase(),
|
||||
birthDate: isoBirthDate,
|
||||
passportSerial: `${data.passportSeriya.toUpperCase()}${
|
||||
data.passportNumber
|
||||
}`,
|
||||
passportPin: data.jshshir,
|
||||
passportFrontImage: frontImage ? `${frontImage.base64}` : '',
|
||||
passportBackImage: backImage ? `${backImage.base64}` : '',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ImageBackground
|
||||
source={Logo}
|
||||
style={RegisterStyle.background}
|
||||
resizeMode="contain"
|
||||
imageStyle={{
|
||||
opacity: 0.3,
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
transform: [{ scale: 1.5 }],
|
||||
}}
|
||||
>
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<View style={RegisterStyle.langContainer}>
|
||||
<TouchableOpacity onPress={() => navigation.goBack()}>
|
||||
<SimpleLineIcons name="arrow-left" color="#000" size={20} />
|
||||
</TouchableOpacity>
|
||||
<LanguageSelector />
|
||||
</View>
|
||||
|
||||
<KeyboardAvoidingView
|
||||
style={RegisterStyle.container}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
>
|
||||
<ScrollView style={RegisterStyle.content}>
|
||||
<View style={RegisterStyle.scrollContainer}>
|
||||
<View style={RegisterStyle.loginContainer}>
|
||||
<Text style={RegisterStyle.title}>
|
||||
{t('Shaxsiy maʼlumotlar')}
|
||||
</Text>
|
||||
<Image
|
||||
source={require('screens/../../assets/bootsplash/passport-sample.jpg')}
|
||||
style={{ width: '100%', height: 300, borderRadius: 8 }}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
<View>
|
||||
{/* PASSPORT */}
|
||||
<Text style={RegisterStyle.label}>
|
||||
{t('Passport seriya raqami')}
|
||||
</Text>
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="passportSeriya"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextInput
|
||||
style={[
|
||||
RegisterStyle.input,
|
||||
RegisterStyle.seriyaInput,
|
||||
]}
|
||||
placeholder="AA"
|
||||
maxLength={2}
|
||||
autoCapitalize="characters"
|
||||
value={value}
|
||||
onChangeText={text => {
|
||||
onChange(text);
|
||||
if (text.length === 2)
|
||||
passportNumberRef.current?.focus();
|
||||
}}
|
||||
placeholderTextColor="#D8DADC"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="passportNumber"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextInput
|
||||
ref={passportNumberRef}
|
||||
style={[
|
||||
RegisterStyle.input,
|
||||
RegisterStyle.raqamInput,
|
||||
]}
|
||||
placeholder="1234567"
|
||||
maxLength={7}
|
||||
keyboardType="numeric"
|
||||
value={value}
|
||||
onChangeText={text => {
|
||||
const onlyNumbers = text.replace(/[^0-9]/g, '');
|
||||
onChange(onlyNumbers);
|
||||
}}
|
||||
placeholderTextColor="#D8DADC"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
{(errors.passportSeriya || errors.passportNumber) && (
|
||||
<Text style={RegisterStyle.errorText}>
|
||||
{t(errors.passportSeriya?.message || '') ||
|
||||
t(errors.passportNumber?.message || '')}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
{/* JSHSHIR */}
|
||||
<Controller
|
||||
control={control}
|
||||
name="jshshir"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<View>
|
||||
<Text style={RegisterStyle.label}>{t('JSHSHIR')}</Text>
|
||||
<TextInput
|
||||
style={RegisterStyle.input}
|
||||
placeholder="12345678901234"
|
||||
placeholderTextColor="#D8DADC"
|
||||
keyboardType="numeric"
|
||||
maxLength={14}
|
||||
value={value}
|
||||
onChangeText={text =>
|
||||
onChange(text.replace(/[^0-9]/g, ''))
|
||||
}
|
||||
/>
|
||||
{errors.jshshir && (
|
||||
<Text style={RegisterStyle.errorText}>
|
||||
{t(errors.jshshir.message || '')}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* BIRTH DATE */}
|
||||
<Controller
|
||||
control={control}
|
||||
name="birthDate"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<View style={{ marginBottom: 10 }}>
|
||||
<Text style={RegisterStyle.label}>
|
||||
{t("Tug'ilgan sana")}
|
||||
</Text>
|
||||
|
||||
<View
|
||||
style={[
|
||||
RegisterStyle.inputContainer,
|
||||
{ paddingHorizontal: 0 },
|
||||
]}
|
||||
>
|
||||
<TextInput
|
||||
style={[
|
||||
RegisterStyle.input,
|
||||
{ flex: 1, borderWidth: 0 },
|
||||
]}
|
||||
placeholder="dd/mm/yyyy"
|
||||
placeholderTextColor="#D8DADC"
|
||||
keyboardType="numeric"
|
||||
value={value}
|
||||
onChangeText={text => {
|
||||
let cleaned = text
|
||||
.replace(/[^\d]/g, '')
|
||||
.slice(0, 8);
|
||||
let formatted = '';
|
||||
|
||||
// Early validation for day and month before formatting
|
||||
if (cleaned.length >= 1) {
|
||||
const firstDigit = cleaned[0];
|
||||
if (firstDigit > '3') return; // Day can't start with 4-9
|
||||
}
|
||||
|
||||
if (cleaned.length >= 2) {
|
||||
const day = parseInt(cleaned.slice(0, 2), 10);
|
||||
if (day > 31 || day === 0) return;
|
||||
}
|
||||
|
||||
if (cleaned.length >= 3) {
|
||||
const monthFirstDigit = cleaned[2];
|
||||
if (monthFirstDigit > '1') return; // Month can't start with 2-9
|
||||
}
|
||||
|
||||
if (cleaned.length >= 4) {
|
||||
const month = parseInt(cleaned.slice(2, 4), 10);
|
||||
if (month > 12 || month === 0) return;
|
||||
}
|
||||
|
||||
// Now format it after initial checks
|
||||
if (cleaned.length <= 2) {
|
||||
formatted = cleaned;
|
||||
} else if (cleaned.length <= 4) {
|
||||
formatted = `${cleaned.slice(
|
||||
0,
|
||||
2,
|
||||
)}/${cleaned.slice(2)}`;
|
||||
} else {
|
||||
formatted = `${cleaned.slice(
|
||||
0,
|
||||
2,
|
||||
)}/${cleaned.slice(2, 4)}/${cleaned.slice(4)}`;
|
||||
}
|
||||
|
||||
// Validate full date not in future
|
||||
if (formatted.length === 10) {
|
||||
const [d, m, y] = formatted.split('/');
|
||||
const inputDate = new Date(+y, +m - 1, +d);
|
||||
const today = new Date();
|
||||
inputDate.setHours(0, 0, 0, 0);
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
if (inputDate > today) return;
|
||||
}
|
||||
|
||||
setValue('birthDate', formatted);
|
||||
}}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
onPress={() => setDatePickerVisibility(true)}
|
||||
style={{ right: 15 }}
|
||||
>
|
||||
<AntDesign
|
||||
name="calendar"
|
||||
color="#D8DADC"
|
||||
size={25}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{errors.birthDate && (
|
||||
<Text style={RegisterStyle.errorText}>
|
||||
{t(errors.birthDate?.message || '')}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
|
||||
<DatePickerInput
|
||||
showPicker={isDatePickerVisible}
|
||||
setShowPicker={setDatePickerVisibility}
|
||||
value={selectedDate || new Date()}
|
||||
onChange={date => {
|
||||
if (date) {
|
||||
const formattedDate = formatDate(date);
|
||||
setSelectedDate(date);
|
||||
setInputValue(formattedDate);
|
||||
setValue('birthDate', formattedDate);
|
||||
}
|
||||
}}
|
||||
maximumDate={new Date()}
|
||||
/>
|
||||
|
||||
{/* FILE UPLOAD */}
|
||||
<Text style={RegisterStyle.mainTitle}>
|
||||
{t('Passport/ID karta rasmi yoki faylni yuklang')}
|
||||
</Text>
|
||||
<View
|
||||
style={[
|
||||
RegisterStyle.sectionsContainer,
|
||||
{ flexDirection: isSmallScreen ? 'column' : 'row' },
|
||||
]}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: isSmallScreen ? '100%' : '48%',
|
||||
marginBottom: 10,
|
||||
}}
|
||||
>
|
||||
<SingleFileDrop
|
||||
title={t('Old tomon')}
|
||||
onFileSelected={setFrontImage}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
width: isSmallScreen ? '100%' : '48%',
|
||||
marginBottom: 10,
|
||||
}}
|
||||
>
|
||||
<SingleFileDrop
|
||||
title={t('Orqa tomon')}
|
||||
onFileSelected={setBackImage}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* BUTTON */}
|
||||
<TouchableOpacity
|
||||
onPress={handleSubmit(onSubmit)}
|
||||
style={[
|
||||
RegisterStyle.button,
|
||||
!termsAccepted && RegisterStyle.buttonDisabled,
|
||||
]}
|
||||
disabled={!termsAccepted}
|
||||
>
|
||||
{isPending ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<Text
|
||||
style={[
|
||||
RegisterStyle.btnText,
|
||||
!termsAccepted && RegisterStyle.buttonTextDisabled,
|
||||
]}
|
||||
>
|
||||
{t('Tasdiqlash')}
|
||||
</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
</ImageBackground>
|
||||
);
|
||||
};
|
||||
|
||||
export default SecondStep;
|
||||
155
src/screens/auth/registeration/ui/TermsAndConditions.tsx
Normal file
155
src/screens/auth/registeration/ui/TermsAndConditions.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import SimpleLineIcons from 'react-native-vector-icons/SimpleLineIcons';
|
||||
import { RootStackParamList } from 'types/types';
|
||||
|
||||
type TermsScreenNavigationProp = NativeStackNavigationProp<
|
||||
RootStackParamList,
|
||||
'TermsAndConditions'
|
||||
>;
|
||||
|
||||
const TermsAndConditions = () => {
|
||||
const navigation = useNavigation<TermsScreenNavigationProp>();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleAgree = () => {
|
||||
navigation.goBack();
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity onPress={() => navigation.goBack()}>
|
||||
<SimpleLineIcons name="arrow-left" color="#000" size={20} />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.headerTitle}>{t('Foydalanish shartlari')}</Text>
|
||||
<View style={{ width: 20 }} />
|
||||
</View>
|
||||
|
||||
<ScrollView style={styles.content} showsVerticalScrollIndicator={false}>
|
||||
<Text style={styles.title}>
|
||||
{t('foydalanish_shartlari_va_qoidalari')}
|
||||
</Text>
|
||||
|
||||
<Text style={styles.sectionTitle}>{t('umumiy_qoidalar')}</Text>
|
||||
<Text style={styles.text}>{t('umumiy_qoidalar_text')}</Text>
|
||||
|
||||
<Text style={styles.sectionTitle}>
|
||||
{t('foydalanuvchi_majburiyatlari')}
|
||||
</Text>
|
||||
<Text style={styles.text}>
|
||||
{t('foydalanuvchi_majburiyatlari_text')}
|
||||
</Text>
|
||||
|
||||
<Text style={styles.sectionTitle}>{t('maxfiylik_siyosati')}</Text>
|
||||
<Text style={styles.text}>{t('maxfiylik_siyosati_text')}</Text>
|
||||
|
||||
<Text style={styles.sectionTitle}>{t('javobgarlik')}</Text>
|
||||
<Text style={styles.text}>{t('javobgarlik_text')}</Text>
|
||||
|
||||
<Text style={styles.sectionTitle}>{t('shartlarni_ozgartirish')}</Text>
|
||||
<Text style={styles.text}>{t('shartlarni_ozgartirish_text')}</Text>
|
||||
|
||||
<Text style={styles.sectionTitle}>{t('aloqa')}</Text>
|
||||
<Text style={styles.text}>{t('aloqa_text')}</Text>
|
||||
|
||||
<View style={styles.footer}>
|
||||
<Text style={styles.footerText}>
|
||||
{t('oxirgi_yangilanish')} {new Date().toLocaleDateString('uz-UZ')}
|
||||
</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
<View style={styles.bottomContainer}>
|
||||
<TouchableOpacity style={styles.agreeButton} onPress={handleAgree}>
|
||||
<Text style={styles.agreeButtonText}>{t('roziman')}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 15,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#E5E5E5',
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: '#000',
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
color: '#000',
|
||||
marginVertical: 20,
|
||||
textAlign: 'center',
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: '#000',
|
||||
marginTop: 20,
|
||||
marginBottom: 10,
|
||||
},
|
||||
text: {
|
||||
fontSize: 14,
|
||||
lineHeight: 22,
|
||||
color: '#333',
|
||||
marginBottom: 15,
|
||||
textAlign: 'justify',
|
||||
},
|
||||
footer: {
|
||||
marginTop: 30,
|
||||
marginBottom: 20,
|
||||
alignItems: 'center',
|
||||
},
|
||||
footerText: {
|
||||
fontSize: 12,
|
||||
color: '#666',
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
bottomContainer: {
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 15,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#E5E5E5',
|
||||
},
|
||||
agreeButton: {
|
||||
backgroundColor: '#28A7E8',
|
||||
paddingVertical: 15,
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
},
|
||||
agreeButtonText: {
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
||||
export default TermsAndConditions;
|
||||
24
src/screens/auth/registeration/ui/index.tsx
Normal file
24
src/screens/auth/registeration/ui/index.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Confirm from './Confirm';
|
||||
import FirstStep from './FirstStep';
|
||||
|
||||
const Register = () => {
|
||||
const [step, setStep] = useState<number>(1);
|
||||
|
||||
const nextStep = () => {
|
||||
if (step === 1) {
|
||||
setStep(2);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{step === 1 && <FirstStep onNext={nextStep} />}
|
||||
{step === 2 && <Confirm setStep={setStep} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Register;
|
||||
210
src/screens/auth/registeration/ui/styled.ts
Normal file
210
src/screens/auth/registeration/ui/styled.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
export const RegisterStyle = StyleSheet.create({
|
||||
langContainer: {
|
||||
width: '100%',
|
||||
marginTop: 10,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
background: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
scrollContainer: {
|
||||
flexGrow: 1,
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
},
|
||||
loginContainer: {
|
||||
borderRadius: 20,
|
||||
padding: 30,
|
||||
display: 'flex',
|
||||
gap: 20,
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
},
|
||||
sectionsContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
gap: 15,
|
||||
},
|
||||
mainTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
color: '#28A7E8',
|
||||
marginBottom: 10,
|
||||
},
|
||||
label: {
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
color: '#28A7E8',
|
||||
marginBottom: 5,
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
color: '#28A7E8',
|
||||
marginBottom: 20,
|
||||
},
|
||||
errorText: {
|
||||
color: 'red',
|
||||
fontSize: 12,
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#D8DADC',
|
||||
borderRadius: 12,
|
||||
padding: 15,
|
||||
fontSize: 16,
|
||||
fontWeight: '400',
|
||||
backgroundColor: '#FFFFFF',
|
||||
color: '#000',
|
||||
},
|
||||
seriyaInput: {
|
||||
width: 60,
|
||||
fontSize: 14,
|
||||
textTransform: 'uppercase',
|
||||
borderTopRightRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
},
|
||||
raqamInput: {
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
},
|
||||
selector: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
},
|
||||
selectedText: {
|
||||
fontSize: 14,
|
||||
color: '#D8DADC',
|
||||
},
|
||||
dropdown: {
|
||||
position: 'absolute',
|
||||
top: 80,
|
||||
left: 0,
|
||||
right: 0,
|
||||
backgroundColor: '#fff',
|
||||
borderWidth: 1,
|
||||
borderColor: '#ccc',
|
||||
borderRadius: 8,
|
||||
zIndex: 10,
|
||||
elevation: 5,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 4,
|
||||
maxHeight: 150,
|
||||
},
|
||||
dropdownItem: {
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 12,
|
||||
},
|
||||
dropdownItemText: {
|
||||
fontSize: 14,
|
||||
color: '#333',
|
||||
},
|
||||
button: {
|
||||
backgroundColor: '#28A7E8',
|
||||
height: 56,
|
||||
borderRadius: 8,
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
btnText: {
|
||||
color: '#fff',
|
||||
fontSize: 20,
|
||||
textAlign: 'center',
|
||||
},
|
||||
termsContainer: {
|
||||
marginVertical: 15,
|
||||
paddingHorizontal: 5,
|
||||
},
|
||||
checkboxContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
paddingVertical: 5,
|
||||
},
|
||||
checkbox: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderWidth: 2.5,
|
||||
borderColor: '#E0E4E7',
|
||||
borderRadius: 6,
|
||||
marginRight: 12,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginTop: 1,
|
||||
backgroundColor: '#FFFFFF',
|
||||
elevation: 2,
|
||||
},
|
||||
checkboxChecked: {
|
||||
backgroundColor: '#28A7E8',
|
||||
borderColor: '#28A7E8',
|
||||
},
|
||||
termsTextContainer: {
|
||||
flex: 1,
|
||||
paddingTop: 2,
|
||||
},
|
||||
termsText: {
|
||||
fontSize: 15,
|
||||
color: '#2C3E50',
|
||||
lineHeight: 22,
|
||||
fontWeight: '400',
|
||||
},
|
||||
termsLink: {
|
||||
color: '#28A7E8',
|
||||
fontWeight: '600',
|
||||
textDecorationLine: 'underline',
|
||||
fontSize: 15,
|
||||
},
|
||||
buttonDisabled: {
|
||||
backgroundColor: '#28A7E8',
|
||||
opacity: 0.7,
|
||||
},
|
||||
buttonTextDisabled: {
|
||||
color: 'white',
|
||||
},
|
||||
btnRegister: {
|
||||
color: '#28A7E8',
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
},
|
||||
inputContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: '#D8DADC',
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 15,
|
||||
height: 56,
|
||||
backgroundColor: '#FFFFFF',
|
||||
},
|
||||
iconButton: {
|
||||
position: 'absolute',
|
||||
right: 12,
|
||||
top: '50%',
|
||||
transform: [{ translateY: -12 }],
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: 24,
|
||||
width: 24,
|
||||
},
|
||||
});
|
||||
146
src/screens/auth/select-auth/SelectAuth.tsx
Normal file
146
src/screens/auth/select-auth/SelectAuth.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Image,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
useWindowDimensions,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import Logo from 'screens/../../assets/bootsplash/logo_512.png';
|
||||
import { RootStackParamList } from 'types/types';
|
||||
|
||||
type LoginScreenNavigationProp = NativeStackNavigationProp<
|
||||
RootStackParamList,
|
||||
'Login'
|
||||
>;
|
||||
|
||||
const SelectAuth = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigation = useNavigation<LoginScreenNavigationProp>();
|
||||
const { width } = useWindowDimensions();
|
||||
|
||||
const isSmallScreen = width < 360;
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<View style={styles.container}>
|
||||
<ScrollView
|
||||
style={{ flex: 1 }}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={[styles.innerContainer, { maxWidth: 500 }]}>
|
||||
<View style={styles.logoWrapper}>
|
||||
<Image
|
||||
source={Logo}
|
||||
style={[
|
||||
styles.logoImage,
|
||||
{
|
||||
width: isSmallScreen ? 120 : 250,
|
||||
height: isSmallScreen ? 120 : 200,
|
||||
borderRadius: 20000,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Text
|
||||
style={[styles.logoText, { fontSize: isSmallScreen ? 22 : 50 }]}
|
||||
>
|
||||
CPOST
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Text style={[styles.title, { fontSize: isSmallScreen ? 20 : 24 }]}>
|
||||
{t('Ro’yxatdan o’tganmisz')}
|
||||
</Text>
|
||||
|
||||
<View style={styles.btnContainer}>
|
||||
<View style={{ gap: 4 }}>
|
||||
<Text style={styles.helperText}>
|
||||
{t("Botdan ro'yxatdan o’tganmisiz")}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
onPress={() => navigation.navigate('Login')}
|
||||
style={styles.button}
|
||||
>
|
||||
<Text style={styles.btnText}>{t('Tizimga kirish')}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={{ gap: 4 }}>
|
||||
<Text style={styles.helperText}>
|
||||
{t("Yangi ro’yxatdan o'tmoqchimisiz")}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
onPress={() => navigation.navigate('Register')}
|
||||
style={styles.button}
|
||||
>
|
||||
<Text style={styles.btnText}>{t('Ro’yxatdan o’tish')}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectAuth;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 20,
|
||||
margin: 5,
|
||||
borderRadius: 12,
|
||||
},
|
||||
scrollContent: {
|
||||
flexGrow: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
innerContainer: {
|
||||
width: '100%',
|
||||
paddingHorizontal: 10,
|
||||
},
|
||||
logoWrapper: {
|
||||
alignItems: 'center',
|
||||
marginBottom: 40,
|
||||
},
|
||||
logoImage: {
|
||||
resizeMode: 'stretch',
|
||||
},
|
||||
logoText: {
|
||||
fontWeight: '700',
|
||||
color: '#28A7E8',
|
||||
},
|
||||
title: {
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
color: '#28A7E8',
|
||||
marginBottom: 20,
|
||||
},
|
||||
helperText: {
|
||||
color: '#28A7E8',
|
||||
fontSize: 16,
|
||||
},
|
||||
button: {
|
||||
backgroundColor: '#28A7E8',
|
||||
height: 56,
|
||||
borderRadius: 6,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
btnText: {
|
||||
color: '#fff',
|
||||
fontSize: 18,
|
||||
textAlign: 'center',
|
||||
},
|
||||
btnContainer: {
|
||||
gap: 16,
|
||||
},
|
||||
});
|
||||
120
src/screens/auth/select-language/SelectLang.tsx
Normal file
120
src/screens/auth/select-language/SelectLang.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/Feather';
|
||||
import RU from 'screens/../../assets/bootsplash/RU.png';
|
||||
import UZ from 'screens/../../assets/bootsplash/UZ.png';
|
||||
import { changeLanguage } from 'utils/changeLanguage';
|
||||
|
||||
const languages = [
|
||||
{ code: 'uz', label: 'Uzb', Icon: UZ },
|
||||
{ code: 'ru', label: 'Ru', Icon: RU },
|
||||
];
|
||||
|
||||
const LanguageSelector = () => {
|
||||
const { i18n } = useTranslation();
|
||||
const [dropdownVisible, setDropdownVisible] = useState(false);
|
||||
|
||||
const selectedLang = languages.find(l => l.code === i18n.language);
|
||||
|
||||
const handleLanguageChange = async (lang: string) => {
|
||||
await changeLanguage(lang);
|
||||
setDropdownVisible(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.wrapper}>
|
||||
<TouchableOpacity
|
||||
style={styles.selector}
|
||||
onPress={() => setDropdownVisible(prev => !prev)}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: 10,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={selectedLang?.Icon}
|
||||
style={{ width: 20, height: 20, objectFit: 'contain' }}
|
||||
/>
|
||||
<Text style={styles.selectedText}>{selectedLang?.label}</Text>
|
||||
</View>
|
||||
<Icon
|
||||
name={dropdownVisible ? 'chevron-up' : 'chevron-down'}
|
||||
size={20}
|
||||
color="#555"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
{dropdownVisible && (
|
||||
<View style={styles.dropdown}>
|
||||
{languages.map(item => (
|
||||
<TouchableOpacity
|
||||
key={item.code}
|
||||
style={styles.dropdownItem}
|
||||
onPress={() => handleLanguageChange(item.code)}
|
||||
>
|
||||
<Image
|
||||
source={item.Icon}
|
||||
style={{ width: 25, height: 25, objectFit: 'contain' }}
|
||||
/>
|
||||
<Text style={styles.dropdownItemText}>{item.label}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
wrapper: {
|
||||
alignSelf: 'flex-end',
|
||||
margin: 10,
|
||||
width: 160,
|
||||
},
|
||||
selector: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#ccc',
|
||||
borderRadius: 8,
|
||||
padding: 10,
|
||||
backgroundColor: '#fff',
|
||||
flexDirection: 'row',
|
||||
gap: 10,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
alignSelf: 'flex-end',
|
||||
},
|
||||
selectedText: {
|
||||
fontSize: 14,
|
||||
color: '#333',
|
||||
},
|
||||
dropdown: {
|
||||
position: 'absolute',
|
||||
top: 45,
|
||||
right: 0,
|
||||
borderWidth: 1,
|
||||
borderColor: '#ccc',
|
||||
borderRadius: 6,
|
||||
backgroundColor: '#fff',
|
||||
zIndex: 999,
|
||||
width: 110,
|
||||
},
|
||||
dropdownItem: {
|
||||
paddingVertical: 5,
|
||||
paddingHorizontal: 5,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 5,
|
||||
},
|
||||
dropdownItemText: {
|
||||
fontSize: 14,
|
||||
color: '#333',
|
||||
},
|
||||
});
|
||||
|
||||
export default LanguageSelector;
|
||||
151
src/screens/auth/select-language/SelectLangPage.tsx
Normal file
151
src/screens/auth/select-language/SelectLangPage.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import React from 'react';
|
||||
import {
|
||||
Image,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
useWindowDimensions,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import RU from 'screens/../../assets/bootsplash/RU.png';
|
||||
import UZ from 'screens/../../assets/bootsplash/UZ.png';
|
||||
import Logo from 'screens/../../assets/bootsplash/logo_512.png';
|
||||
import { RootStackParamList } from 'types/types';
|
||||
import { changeLanguage } from 'utils/changeLanguage';
|
||||
|
||||
type NavigationProp = NativeStackNavigationProp<RootStackParamList>;
|
||||
|
||||
const SelectLangPage = () => {
|
||||
const navigation = useNavigation<NavigationProp>();
|
||||
const { width } = useWindowDimensions();
|
||||
|
||||
const isSmallScreen = width < 380;
|
||||
|
||||
const selectLanguage = async (lang: 'uz' | 'ru') => {
|
||||
await changeLanguage(lang);
|
||||
navigation.navigate('select-auth');
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<View style={styles.container}>
|
||||
<ScrollView
|
||||
style={{ flex: 1 }}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={[styles.innerContainer, { maxWidth: 500 }]}>
|
||||
<View style={styles.logoWrapper}>
|
||||
<Image
|
||||
source={Logo}
|
||||
style={[
|
||||
styles.logoImage,
|
||||
{
|
||||
width: isSmallScreen ? 120 : 250,
|
||||
height: isSmallScreen ? 120 : 200,
|
||||
borderRadius: 20000,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Text
|
||||
style={[styles.logoText, { fontSize: isSmallScreen ? 24 : 40 }]}
|
||||
>
|
||||
CPOST
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Text style={[styles.title, { fontSize: isSmallScreen ? 18 : 24 }]}>
|
||||
Tilni tanlang{' '}
|
||||
<Text
|
||||
style={[styles.title, { fontSize: isSmallScreen ? 14 : 18 }]}
|
||||
>
|
||||
(Выберите язык)
|
||||
</Text>
|
||||
</Text>
|
||||
|
||||
<View style={styles.btnContainer}>
|
||||
<TouchableOpacity
|
||||
onPress={() => selectLanguage('uz')}
|
||||
style={styles.button}
|
||||
>
|
||||
<Image source={UZ} style={styles.flag} />
|
||||
<Text style={styles.btnText}>O'zbek tili</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => selectLanguage('ru')}
|
||||
style={styles.button}
|
||||
>
|
||||
<Image source={RU} style={styles.flag} />
|
||||
<Text style={styles.btnText}>Русский язык</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectLangPage;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 20,
|
||||
margin: 5,
|
||||
borderRadius: 12,
|
||||
},
|
||||
scrollContent: {
|
||||
flexGrow: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
innerContainer: {
|
||||
width: '100%',
|
||||
paddingHorizontal: 10,
|
||||
},
|
||||
logoWrapper: {
|
||||
alignItems: 'center',
|
||||
marginBottom: 30,
|
||||
},
|
||||
logoImage: {
|
||||
resizeMode: 'stretch',
|
||||
marginBottom: 8,
|
||||
},
|
||||
logoText: {
|
||||
fontWeight: '700',
|
||||
color: '#28A7E8',
|
||||
},
|
||||
title: {
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
color: '#28A7E8',
|
||||
marginBottom: 24,
|
||||
},
|
||||
button: {
|
||||
backgroundColor: '#28A7E8',
|
||||
height: 56,
|
||||
borderRadius: 6,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
btnText: {
|
||||
color: '#fff',
|
||||
fontSize: 18,
|
||||
},
|
||||
btnContainer: {
|
||||
gap: 16,
|
||||
},
|
||||
flag: {
|
||||
width: 30,
|
||||
height: 30,
|
||||
resizeMode: 'cover',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user