Initial commit

This commit is contained in:
Samandar Turgunboyev
2025-08-26 16:26:59 +05:00
commit fd95422447
318 changed files with 38301 additions and 0 deletions

13
src/api/URL.ts Normal file
View File

@@ -0,0 +1,13 @@
export const BASE_URL = 'https://api.cpcargo.uz/api/v1';
export const REGISTER = '/mobile/auth/register';
export const LOGIN = '/mobile/auth/login';
export const BRANCH_LIST = '/mobile/branch/list';
export const SEND_PASSPORT = '/mobile/profile/send-passport';
export const ADD_PASSPORT = '/mobile/profile/add-passport';
export const PASSPORT_ME = '/mobile/profile/passports';
export const VERIFY_OTP = '/mobile/auth/verify-otp';
export const RESEND_OTP = '/mobile/auth/resend-otp';
export const CALENDAR = '/mobile/calendar';
export const PACKETS = '/mobile/packets';
export const GET_ME = '/mobile/profile/me';

50
src/api/auth/index.ts Normal file
View File

@@ -0,0 +1,50 @@
import axiosInstance from 'api/axios';
import { GET_ME, LOGIN, REGISTER, RESEND_OTP, VERIFY_OTP } from 'api/URL';
import {
getMeData,
loginPayload,
otpPayload,
registerPayload,
resendPayload,
} from './type';
const auth = {
async register(payload: registerPayload) {
try {
const data = await axiosInstance.post(REGISTER, payload);
return data;
} catch (error) {
throw error;
}
},
async verifyOtp(payload: otpPayload) {
try {
const data = await axiosInstance.post(VERIFY_OTP, payload);
return data;
} catch (error) {
throw error;
}
},
async resendOtp(payload: resendPayload) {
try {
const data = await axiosInstance.post(RESEND_OTP, payload);
return data;
} catch (error) {
throw error;
}
},
async login(payload: loginPayload) {
try {
const data = await axiosInstance.post(LOGIN, payload);
return data;
} catch (error) {
throw error;
}
},
async getMe(): Promise<getMeData> {
const { data } = await axiosInstance.get(GET_ME);
return data;
},
};
export const authApi = auth;

38
src/api/auth/type.ts Normal file
View File

@@ -0,0 +1,38 @@
import { myPassport } from 'api/passport';
export interface registerPayload {
firstName: string;
lastName: string;
phoneNumber: string;
recommend: string;
branchId: number;
}
export interface otpPayload {
phoneNumber: string;
otp: string;
otpType: 'LOGIN' | 'RESET_PASSWORD' | 'REGISTRATION';
}
export interface resendPayload {
phoneNumber: string;
otpType?: 'LOGIN' | 'RESET_PASSWORD' | 'REGISTRATION';
}
export interface loginPayload {
phoneNumber: string;
passportSerial: string;
// passportNumber: string;
branchId: number;
}
export interface getMeData {
id: number;
fullName: string;
phone: string;
address: string;
dateOfBirth: string;
passport: myPassport[];
aviaCargoId: string;
autoCargoId: string;
status: string;
}

40
src/api/axios.ts Normal file
View File

@@ -0,0 +1,40 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import axios, { AxiosError } from 'axios';
import { navigate } from 'components/NavigationRef';
const axiosInstance = axios.create({
baseURL: 'https://api.cpcargo.uz/api/v1',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
axiosInstance.interceptors.request.use(async config => {
// Tokenni olish
const token = await AsyncStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// Languageni olish
const language = await AsyncStorage.getItem('language');
if (language) {
config.headers['Accept-Language'] = language;
}
return config;
});
axiosInstance.interceptors.response.use(
response => response,
async (error: AxiosError) => {
if (error.response?.status === 401) {
await AsyncStorage.removeItem('token');
navigate('Login');
}
return Promise.reject(error);
},
);
export default axiosInstance;

28
src/api/branch/index.ts Normal file
View File

@@ -0,0 +1,28 @@
import axiosInstance from 'api/axios';
import { BRANCH_LIST } from 'api/URL';
export interface Branch {
address: string | null;
code: string;
id: number;
latitude: number;
longitude: number;
name: string;
phone: string;
telegramAdmin: string;
telegramChannel: string;
workingHours: string;
}
const branch = {
async branchList(): Promise<Branch[]> {
try {
const response = await axiosInstance.get<Branch[]>(BRANCH_LIST);
return response.data;
} catch (error) {
return [];
}
},
};
export const branchApi = branch;

33
src/api/calendar/index.ts Normal file
View File

@@ -0,0 +1,33 @@
import axiosInstance from 'api/axios';
import { CALENDAR } from 'api/URL';
export interface CalendarData {
id: 0;
monday: string;
tuesday: string;
wednesday: string;
thursday: string;
friday: string;
saturday: string;
sunday: string;
weekStartDate: string;
createdAt: string;
updatedAt: string;
isActive: boolean;
cargoType: string;
}
interface Params {
cargoType: 'AUTO' | 'AVIA';
}
const calendarAPi = {
async getCalendar(params: Params): Promise<CalendarData> {
const response = await axiosInstance.get<CalendarData>(CALENDAR, {
params,
});
return response.data;
},
};
export default calendarAPi;

60
src/api/packets/index.ts Normal file
View File

@@ -0,0 +1,60 @@
import axiosInstance from 'api/axios';
import { PACKETS } from 'api/URL';
export interface PacketsData {
data: {
id: number;
packetName: string;
weight: number;
totalPrice: number;
paymentStatus: string;
paymentType: string;
deliveryStatus: string;
items: {
trekId: string;
name: string;
weight: number;
price: number;
totalPrice: number;
}[];
qrCode: string;
}[];
totalPages: number;
totalElements: number;
}
interface Params {
cargoType: 'AUTO' | 'AVIA';
page: number;
size: number;
sort: string;
direction: string;
}
export interface payPayload {
payType: string;
}
const packetsApi = {
async getPackets(params: Params): Promise<PacketsData> {
const response = await axiosInstance.get<PacketsData>(PACKETS, { params });
return response.data;
},
async payPackets(id: number, payload: payPayload) {
try {
const data = await axiosInstance.post(`${PACKETS}/${id}/pay`, payload);
return data;
} catch (error) {
throw error;
}
},
async getPacketsStatus(status: string, params: any): Promise<PacketsData> {
const response = await axiosInstance.get<PacketsData>(
`${PACKETS}/${status}`,
{ params },
);
return response.data;
},
};
export default packetsApi;

50
src/api/passport/index.ts Normal file
View File

@@ -0,0 +1,50 @@
import axiosInstance from 'api/axios';
import { ADD_PASSPORT, PASSPORT_ME, SEND_PASSPORT } from 'api/URL';
export interface sendPassportPayload {
fullName: string;
birthDate: string;
passportSerial: string;
passportPin: string;
passportFrontImage: string;
passportBackImage: string;
}
export interface AddPassportPayload {
fullName: string;
birthDate: string;
passportSerial: string;
passportPin: string;
passportFrontImage: string;
passportBackImage: string;
}
export interface myPassport {
fullName: string;
passportSeries: string;
passportPin: string;
passportFrontImage: string;
passportBackImage: string;
address: string;
phone: string;
birthDate: string;
availableLimit: number;
active: boolean;
}
const passportApi = {
async sendPassport(payload: sendPassportPayload) {
const data = await axiosInstance.post(SEND_PASSPORT, payload);
return data;
},
async getPassport(): Promise<myPassport[]> {
const { data } = await axiosInstance.get<myPassport[]>(PASSPORT_ME);
return data;
},
async addPassport(payload: AddPassportPayload) {
const data = await axiosInstance.post(ADD_PASSPORT, payload);
return data;
},
};
export default passportApi;

View File

@@ -0,0 +1,66 @@
"use client"
import React, { useEffect, useRef, useMemo, useCallback } from "react"
import { View, Animated, StyleSheet } from "react-native"
const AnimatedDots = () => {
const dot1 = useRef(new Animated.Value(0)).current
const dot2 = useRef(new Animated.Value(0)).current
const dot3 = useRef(new Animated.Value(0)).current
const animationConfig = useMemo(() => ({
duration: 300,
useNativeDriver: true,
}), []);
const createTimingAnimation = useCallback((value: Animated.Value, toValue: number) => {
return Animated.timing(value, {
toValue,
...animationConfig,
});
}, [animationConfig]);
const animationSequence = useMemo(() => {
return Animated.sequence([
createTimingAnimation(dot1, 1),
createTimingAnimation(dot2, 1),
createTimingAnimation(dot3, 1),
createTimingAnimation(dot1, 0),
createTimingAnimation(dot2, 0),
createTimingAnimation(dot3, 0),
]);
}, [dot1, dot2, dot3, createTimingAnimation]);
useEffect(() => {
const animateDots = () => {
Animated.loop(animationSequence).start();
};
animateDots();
}, [animationSequence]);
return (
<View style={styles.container}>
<Animated.View style={[styles.dot, { opacity: dot1 }]} />
<Animated.View style={[styles.dot, { opacity: dot2 }]} />
<Animated.View style={[styles.dot, { opacity: dot3 }]} />
</View>
)
}
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
gap: 8,
},
dot: {
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: '#28A7E8',
},
});
export default AnimatedDots;

View File

@@ -0,0 +1,150 @@
"use client"
import React, { useEffect, useState, useCallback, useMemo } from "react"
import { type LayoutChangeEvent, View } from "react-native"
import Animated, {
useSharedValue,
useAnimatedStyle,
withRepeat,
withTiming,
Easing,
withSequence,
withDelay,
} from "react-native-reanimated"
import Auto from "svg/Auto"
import Avia from "svg/Avia"
type Props = {
type: "auto" | "avia"
}
const AnimatedIcon = ({ type }: Props) => {
const translateX = useSharedValue(0)
const translateY = useSharedValue(0)
const rotateY = useSharedValue(0)
const direction = useSharedValue(1)
const [containerWidth, setContainerWidth] = useState(0)
const iconSize = 40
const onLayout = useCallback((event: LayoutChangeEvent) => {
const { width } = event.nativeEvent.layout
setContainerWidth(width)
}, []);
const animationConfig = useMemo(() => ({
duration: 4000,
easing: Easing.linear,
rotationDuration: 300,
arcHeight: -30,
}), []);
const createXAnimation = useCallback((maxX: number) => {
return withRepeat(
withSequence(
withTiming(maxX, {
duration: animationConfig.duration,
easing: animationConfig.easing
}, () => {
direction.value = -1;
rotateY.value = withTiming(180, { duration: animationConfig.rotationDuration });
}),
withTiming(0, {
duration: animationConfig.duration,
easing: animationConfig.easing
}, () => {
direction.value = 1;
rotateY.value = withTiming(0, { duration: animationConfig.rotationDuration });
})
),
-1
);
}, [animationConfig, direction, rotateY]);
const createYAnimation = useCallback(() => {
if (type === "avia") {
return withRepeat(
withSequence(
withTiming(animationConfig.arcHeight, {
duration: animationConfig.duration / 2,
easing: Easing.out(Easing.quad),
}),
withTiming(0, {
duration: animationConfig.duration / 2,
easing: Easing.in(Easing.quad),
}),
withTiming(animationConfig.arcHeight, {
duration: animationConfig.duration / 2,
easing: Easing.out(Easing.quad),
}),
withTiming(0, {
duration: animationConfig.duration / 2,
easing: Easing.in(Easing.quad),
})
),
-1
);
}
return withTiming(0, { duration: 100 });
}, [type, animationConfig]);
useEffect(() => {
if (containerWidth === 0) return;
const maxX = containerWidth - iconSize;
translateX.value = createXAnimation(maxX);
translateY.value = createYAnimation();
}, [containerWidth, type, createXAnimation, createYAnimation, translateX, translateY]);
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value },
{ rotateY: `${rotateY.value}deg` },
],
}
});
const containerStyle = useMemo(() => ({
height: 100,
justifyContent: "center" as const,
backgroundColor: "transparent" as const,
position: "relative" as const,
}), []);
const trackStyle = useMemo(() => ({
height: 2,
backgroundColor: "#28A7E850",
position: "absolute" as const,
top: 25,
left: 0,
right: 0,
}), []);
const iconContainerStyle = useMemo(() => ({
position: "absolute" as const,
top: 0,
height: iconSize,
width: iconSize,
}), [iconSize]);
const renderIcon = useMemo(() => {
if (type === "auto") {
return <Auto color="#28A7E8" width={iconSize} height={iconSize} />;
}
return <Avia color="#28A7E8" width={iconSize} height={iconSize} />;
}, [type, iconSize]);
return (
<View onLayout={onLayout} style={containerStyle}>
<View style={trackStyle} />
<Animated.View style={[iconContainerStyle, animatedStyle]}>
{renderIcon}
</Animated.View>
</View>
)
}
export default AnimatedIcon;

View File

@@ -0,0 +1,71 @@
"use client"
import React, { useEffect, useRef, useMemo, useCallback } from 'react'
import { Animated, Easing, StyleSheet } from 'react-native'
interface AnimatedScreenProps {
children: React.ReactNode
keyIndex: number
}
const AnimatedScreen: React.FC<AnimatedScreenProps> = ({ children, keyIndex }) => {
const opacityAnim = React.useRef(new Animated.Value(1)).current // Start with opacity 1
const slideAnim = React.useRef(new Animated.Value(0)).current // Start with no slide
const animationConfig = useMemo(() => ({
duration: 150, // Further reduced
useNativeDriver: true,
}), []);
const opacityAnimation = useMemo(() =>
Animated.timing(opacityAnim, {
toValue: 1,
duration: 150, // Further reduced
easing: Easing.out(Easing.ease),
useNativeDriver: true,
}), [opacityAnim]);
const slideAnimation = useMemo(() =>
Animated.timing(slideAnim, {
toValue: 0,
duration: 150, // Further reduced
easing: Easing.out(Easing.ease),
useNativeDriver: true,
}), [slideAnim]);
const resetAnimations = useCallback(() => {
opacityAnim.setValue(1); // Start with full opacity
slideAnim.setValue(0); // Start with no slide
}, [opacityAnim, slideAnim]);
const startAnimations = useCallback(() => {
// Skip animations for better performance
// Animated.parallel([opacityAnimation, slideAnimation]).start();
}, [opacityAnimation, slideAnimation]);
useEffect(() => {
resetAnimations();
startAnimations();
}, [keyIndex, resetAnimations, startAnimations]);
const animatedStyle = useMemo(() => ({
opacity: 1, // Always full opacity
transform: [
{ translateX: 0 }, // No slide
],
}), []);
return (
<Animated.View style={[styles.container, animatedStyle]}>
{children}
</Animated.View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
})
export default AnimatedScreen;

View File

@@ -0,0 +1,183 @@
import { Branch } from 'api/branch';
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
Image,
Modal,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import Fontisto from 'react-native-vector-icons/Fontisto';
import Local from 'screens/../../assets/bootsplash/local.png';
import Clock from 'svg/Clock';
import CloseIcon from 'svg/Close';
import Location from 'svg/Location';
import Phone from 'svg/Phone';
type BottomModalProps = {
visible: boolean;
onClose: () => void;
branch: Branch | null;
};
const BottomModal: React.FC<BottomModalProps> = ({
visible,
onClose,
branch,
}) => {
const { t } = useTranslation();
return (
<Modal
animationType="slide"
transparent
visible={visible}
onRequestClose={onClose}
>
<View style={styles.overlay}>
<TouchableOpacity
style={styles.overlayTouchable}
onPress={onClose}
activeOpacity={1}
/>
<View style={[styles.modal]}>
<TouchableOpacity
style={styles.handleContainer}
activeOpacity={0.7}
onPress={onClose}
>
<View style={styles.handle}>
<CloseIcon />
</View>
</TouchableOpacity>
<Image source={Local} style={styles.image} />
<ScrollView contentContainerStyle={styles.scrollContainer}>
<View style={styles.card}>
<View style={styles.row}>
<Location color="#28A7E8" width={26} height={26} />
<View style={styles.cardText}>
<Text style={styles.label}>{t('Manzil')}</Text>
<Text style={styles.value}>{branch?.address}</Text>
</View>
</View>
</View>
<View style={styles.card}>
<View style={styles.row}>
<Clock color="#28A7E8" width={26} height={26} />
<View style={styles.cardText}>
<Text style={styles.label}>{t('Ish vaqti')}</Text>
<Text style={styles.value}>{branch?.workingHours}</Text>
</View>
</View>
</View>
<View style={styles.card}>
<View style={styles.row}>
<Phone color="#28A7E8" width={26} height={26} />
<View style={styles.cardText}>
<Text style={styles.label}>{t('Telefon')}</Text>
<Text style={styles.value}>{branch?.phone}</Text>
</View>
</View>
</View>
<View style={styles.card}>
<View style={styles.row}>
<Fontisto name="telegram" color="#28A7E8" size={26} />
<View style={styles.cardText}>
<Text style={styles.label}>{t('Telegram admin')}</Text>
<Text style={styles.value}>{branch?.telegramAdmin}</Text>
</View>
</View>
</View>
<View style={styles.card}>
<View style={styles.row}>
<Fontisto name="telegram" color="#28A7E8" size={26} />
<View style={styles.cardText}>
<Text style={styles.label}>{t('Telegram kanal')}</Text>
<Text style={styles.value}>{branch?.telegramChannel}</Text>
</View>
</View>
</View>
</ScrollView>
</View>
</View>
</Modal>
);
};
export default BottomModal;
const styles = StyleSheet.create({
overlay: {
flex: 1,
justifyContent: 'flex-end',
backgroundColor: 'rgba(0,0,0,0.4)',
},
overlayTouchable: {
flex: 1,
},
scrollContainer: {},
modal: {
backgroundColor: '#fff',
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
paddingBottom: 30,
elevation: 5,
height: '70%',
},
image: {
width: '100%',
height: 200,
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
},
card: {
backgroundColor: '#F3FAFF',
paddingTop: 5,
paddingRight: 10,
paddingLeft: 10,
paddingBottom: 5,
margin: 'auto',
width: '95%',
marginTop: 10,
borderRadius: 8,
},
cardText: {
width: '90%',
},
row: {
flexDirection: 'row',
alignItems: 'flex-start',
gap: 5,
},
label: {
fontSize: 16,
fontWeight: '600',
color: '#000000',
},
value: {
fontSize: 14,
color: '#000000B2',
fontWeight: '500',
},
handleContainer: {
position: 'absolute',
zIndex: 10,
width: '100%',
alignItems: 'flex-end',
justifyContent: 'flex-end',
paddingVertical: 10,
},
handle: {
width: 25,
height: 25,
right: 10,
justifyContent: 'center',
alignItems: 'center',
borderRadius: 500,
backgroundColor: '#ffff',
},
});

View File

@@ -0,0 +1,58 @@
import React, { useMemo, useCallback } from 'react';
import { StyleSheet, Text, useWindowDimensions, View } from 'react-native';
export const toastConfig = {
success: ({ text1, text2 }: { text1?: string; text2?: string }) => {
const { width: screenWidth } = useWindowDimensions();
const scale = useMemo(() => screenWidth < 360 ? 0.85 : 1, [screenWidth]);
const containerStyle = useMemo(() => [
styles.toastContainer,
{ padding: 10 * scale }
], [scale]);
const text1Style = useMemo(() => [
styles.text1,
{ fontSize: 16 * scale }
], [scale]);
const text2Style = useMemo(() => [
styles.text2,
{ fontSize: 14 * scale }
], [scale]);
const renderText2 = useCallback(() => {
if (!text2) return null;
return <Text style={text2Style}>{text2}</Text>;
}, [text2, text2Style]);
return (
<View style={containerStyle}>
<Text style={text1Style}>{text1}</Text>
{renderText2()}
</View>
);
},
// boshqa turlar uchun (error, info) ham qo'shishingiz mumkin
};
const styles = StyleSheet.create({
toastContainer: {
width: '90%',
backgroundColor: '#4CAF50',
borderRadius: 8,
paddingHorizontal: 15,
shadowColor: '#000',
shadowOpacity: 0.3,
shadowRadius: 6,
shadowOffset: { width: 0, height: 2 },
},
text1: {
color: 'white',
fontWeight: 'bold',
},
text2: {
color: 'white',
marginTop: 2,
},
});

View File

@@ -0,0 +1,141 @@
import DateTimePicker from '@react-native-community/datetimepicker';
import React, { useCallback, useMemo } from 'react';
import {
Modal,
Platform,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
type Props = {
value: Date;
onChange: (date: Date) => void;
showPicker: boolean;
setShowPicker: (showPicker: boolean) => void;
maximumDate?: Date;
};
const DatePickerInput = ({
value,
onChange,
setShowPicker,
maximumDate,
showPicker,
}: Props) => {
const onDateChange = useCallback(
(event: any, selectedDate?: Date) => {
const currentDate = selectedDate || value;
if (Platform.OS !== 'ios') setShowPicker(false);
onChange(currentDate);
},
[value, onChange, setShowPicker],
);
const handleClosePicker = useCallback(() => {
setShowPicker(false);
}, [setShowPicker]);
const renderPicker = useMemo(() => {
if (!showPicker) return null;
if (Platform.OS === 'ios') {
return (
<Modal transparent animationType="slide">
<View style={styles.modalContainer}>
<View style={styles.pickerContainer}>
<DateTimePicker
value={value}
mode="date"
display="spinner"
onChange={onDateChange}
/>
<TouchableOpacity
onPress={handleClosePicker}
style={styles.doneButton}
>
<Text style={styles.doneText}>Done</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
);
}
return (
<>
{showPicker && (
<DateTimePicker
value={value}
mode="date"
display="default"
maximumDate={maximumDate}
onChange={(event, selectedDate) => {
setShowPicker(false);
if (event.type === 'set' && selectedDate) {
onChange(selectedDate);
}
}}
/>
)}
</>
);
}, [showPicker, value, onDateChange, handleClosePicker]);
return <View>{renderPicker}</View>;
};
const styles = StyleSheet.create({
label: { fontSize: 16, fontWeight: '500' },
inputContainer: {
flexDirection: 'row',
alignItems: 'center',
position: 'relative',
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 6,
paddingRight: 36,
},
input: {
flex: 1,
padding: 12,
fontSize: 16,
},
iconButton: {
position: 'absolute',
right: 8,
padding: 8,
},
helperText: {
marginTop: 8,
fontSize: 14,
color: '#555',
},
boldText: {
fontWeight: '600',
},
modalContainer: {
flex: 1,
justifyContent: 'flex-end',
backgroundColor: '#00000055',
},
pickerContainer: {
backgroundColor: 'white',
padding: 16,
borderTopLeftRadius: 12,
borderTopRightRadius: 12,
},
doneButton: {
marginTop: 12,
alignSelf: 'flex-end',
padding: 8,
},
doneText: {
color: '#007AFF',
fontWeight: '600',
fontSize: 16,
},
});
export default DatePickerInput;

224
src/components/FileDrop.tsx Normal file
View File

@@ -0,0 +1,224 @@
import React, { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Alert,
Image,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import {
ImagePickerResponse,
launchImageLibrary,
MediaType,
} from 'react-native-image-picker';
import Feather from 'react-native-vector-icons/Feather';
interface FileData {
uri: string;
name: string;
type: string;
base64: string;
}
interface SingleFileDropProps {
title: string;
onFileSelected?: (file: FileData) => void;
}
const SingleFileDrop: React.FC<SingleFileDropProps> = ({
title,
onFileSelected,
}) => {
const [selectedImage, setSelectedImage] = useState<string | null>(null);
const { t } = useTranslation();
const imagePickerOptions = useMemo(
() => ({
mediaType: 'photo' as MediaType,
includeBase64: true,
}),
[],
);
const handleImagePickerResponse = useCallback(
(response: ImagePickerResponse) => {
if (response.didCancel) return; // foydalanuvchi bekor qilsa
if (response.errorCode) {
Alert.alert('Xato', response.errorMessage || 'Rasmni yuklashda xato');
return;
}
if (response.assets && response.assets[0]) {
const asset = response.assets[0];
if (!asset.uri || !asset.type || !asset.base64) return;
// faqat PNG fayllarni qabul qilish
if (asset.type !== 'image/png') {
Alert.alert('Xato', 'Faqat PNG fayllarni yuklashingiz mumkin');
return;
}
setSelectedImage(asset.uri);
const fileData: FileData = {
uri: asset.uri,
name: asset.fileName || 'image.png',
type: asset.type,
base64: `data:${asset.type};base64,${asset.base64}`,
};
onFileSelected?.(fileData);
}
},
[onFileSelected],
);
const openGallery = useCallback((): void => {
launchImageLibrary(imagePickerOptions, handleImagePickerResponse);
}, [imagePickerOptions, handleImagePickerResponse]);
const UploadIcon = useMemo(
() => () =>
(
<View style={styles.iconContainer}>
<View style={styles.downloadIcon}>
<Feather name="download" color="#28A7E8" size={35} />
</View>
</View>
),
[],
);
const renderContent = useMemo(() => {
if (selectedImage) {
return (
<Image source={{ uri: selectedImage }} style={styles.previewImage} />
);
}
return (
<>
<View style={styles.innerContainer}>
<View style={styles.topContent}>
{selectedImage ? (
<Image
source={{ uri: selectedImage }}
style={styles.previewImage}
/>
) : (
<>
<UploadIcon />
<Text style={styles.sectionTitle}>{title}</Text>
</>
)}
</View>
<View style={styles.bottomContent}>
<View style={styles.dividerContainer}>
<View style={styles.dividerLine} />
<Text style={styles.orDividerText}>OR</Text>
<View style={styles.dividerLine} />
</View>
<View style={styles.browseButton}>
<Text style={styles.browseButtonText}>{t('Faylni yuklang')}</Text>
</View>
</View>
</View>
</>
);
}, [selectedImage, title, UploadIcon]);
return (
<TouchableOpacity style={styles.dropSection} onPress={openGallery}>
{renderContent}
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
dropSection: {
borderWidth: 2,
borderColor: '#28A7E8',
borderStyle: 'dashed',
borderRadius: 8,
padding: 20,
alignItems: 'center',
justifyContent: 'center',
minHeight: 200,
flex: 1,
},
iconContainer: {
marginBottom: 15,
},
downloadIcon: {
width: 40,
height: 40,
borderRadius: 20,
alignItems: 'center',
justifyContent: 'center',
},
iconText: {
fontSize: 20,
color: '#007bff',
fontWeight: 'bold',
},
sectionTitle: {
fontSize: 16,
fontWeight: '500',
color: '#333',
marginBottom: 10,
},
browseButton: {
borderWidth: 1,
borderColor: '#28A7E8',
borderRadius: 8,
padding: 8,
},
browseButtonText: {
color: '#28A7E8',
fontSize: 14,
fontWeight: '500',
},
previewImage: {
width: '100%',
height: 150,
borderRadius: 8,
resizeMode: 'cover',
},
dividerContainer: {
width: '100%',
alignItems: 'center',
justifyContent: 'center',
marginVertical: 16,
position: 'relative',
flexDirection: 'row',
},
dividerLine: {
width: 30,
height: 2,
backgroundColor: '#E7E7E7',
},
orDividerText: {
paddingHorizontal: 12,
fontSize: 14,
color: '#999',
zIndex: 1,
},
innerContainer: {
flex: 1,
justifyContent: 'space-between',
width: '100%',
},
topContent: {
alignItems: 'center',
},
bottomContent: {
width: '100%',
alignItems: 'center',
},
});
export default SingleFileDrop;

View File

@@ -0,0 +1,89 @@
// components/GlobalModal.tsx
import React from 'react';
import { Modal, View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import { useModalStore } from 'screens/auth/registeration/lib/modalStore';
const GlobalModal = () => {
const {
isVisible,
title,
message,
type,
onConfirm,
onCancel,
closeModal,
} = useModalStore();
return (
<Modal
visible={isVisible}
transparent
animationType="fade"
onRequestClose={closeModal}
>
<View style={styles.overlay}>
<View style={styles.modal}>
<Text style={styles.title}>{title}</Text>
<Text style={styles.message}>{message}</Text>
<View style={styles.buttons}>
{onCancel && (
<TouchableOpacity onPress={() => { onCancel(); closeModal(); }}>
<Text style={styles.cancel}>Bekor qilish</Text>
</TouchableOpacity>
)}
<TouchableOpacity
onPress={() => {
onConfirm?.();
closeModal();
}}
>
<Text style={styles.confirm}>Ok</Text>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
);
};
const styles = StyleSheet.create({
overlay: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.4)',
justifyContent: 'center',
alignItems: 'center',
},
modal: {
width: '80%',
padding: 20,
backgroundColor: '#fff',
borderRadius: 10,
elevation: 5,
},
title: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 10,
},
message: {
fontSize: 15,
marginBottom: 20,
},
buttons: {
flexDirection: 'row',
justifyContent: 'flex-end',
gap: 10,
},
cancel: {
color: 'red',
fontWeight: 'bold',
marginRight: 15,
},
confirm: {
color: '#007bff',
fontWeight: 'bold',
},
});
export default GlobalModal;

View File

@@ -0,0 +1,43 @@
import React, { useMemo, useCallback } from 'react';
import { Modal, StyleSheet, View, TouchableOpacity, Text } from 'react-native';
import { WebView } from 'react-native-webview';
type Props = {
visible: boolean;
url: string;
onClose: () => void;
};
const InAppBrowser = ({ visible, url, onClose }: Props) => {
const webViewSource = useMemo(() => ({ uri: url }), [url]);
const handleClose = useCallback(() => {
onClose();
}, [onClose]);
return (
<Modal visible={visible} animationType="slide">
<View style={styles.container}>
<TouchableOpacity style={styles.closeButton} onPress={handleClose}>
<Text style={styles.closeText}>Yopish</Text>
</TouchableOpacity>
<WebView source={webViewSource} />
</View>
</Modal>
);
};
const styles = StyleSheet.create({
container: { flex: 1 },
closeButton: {
padding: 10,
backgroundColor: '#28A7E8',
alignItems: 'center',
},
closeText: {
color: 'white',
fontSize: 16,
},
});
export default InAppBrowser;

View File

@@ -0,0 +1,64 @@
import LottieView from 'lottie-react-native';
import React, { useEffect, useRef } from 'react';
import { Animated, StyleSheet, useWindowDimensions, View } from 'react-native';
import ProgressBar from 'screens/../../assets/lottie/Carry Trolley.json';
interface Props {
message?: string;
progress?: number; // ✅ Qoshimcha prop
}
const LoadingScreen: React.FC<Props> = ({ message, progress = 0 }) => {
const { width } = useWindowDimensions();
const TOTAL_FRAMES = 90;
const scale = width < 360 ? 0.8 : 1;
const translateX = useRef(new Animated.Value(-width)).current;
const progressBarRef = useRef<LottieView>(null);
useEffect(() => {
Animated.loop(
Animated.sequence([
Animated.timing(translateX, {
toValue: width / 2 + 75 * scale,
duration: 6000,
useNativeDriver: true,
}),
Animated.timing(translateX, {
toValue: -width / 2 - 100 * scale,
duration: 0,
useNativeDriver: true,
}),
]),
).start();
}, [translateX, width, scale]);
useEffect(() => {
if (progressBarRef.current) {
progressBarRef.current?.play(0, Math.floor(progress * TOTAL_FRAMES));
}
}, [progress]);
return (
<View style={styles.container}>
<LottieView
source={ProgressBar}
loop
autoPlay={true}
resizeMode="cover"
style={{ width: 150 * scale, height: 150 * scale }}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#fff',
overflow: 'hidden',
},
});
export default LoadingScreen;

175
src/components/Navbar.tsx Normal file
View File

@@ -0,0 +1,175 @@
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import React, { useCallback, useMemo, useState } from 'react';
import {
Dimensions,
Image,
Linking,
Platform,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import AppLink from 'react-native-app-link';
import Fontisto from 'react-native-vector-icons/Fontisto';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import Logo from 'screens/../../assets/bootsplash/logo.png';
import InAppBrowser from './InAppBrowser';
const { width } = Dimensions.get('window');
const isSmallScreen = width < 360;
const Navbar = () => {
const [browserUrl, setBrowserUrl] = useState('');
const [modalVisible, setModalVisible] = useState(false);
const navigation = useNavigation<NativeStackNavigationProp<any>>();
const iconSizes = useMemo(
() => ({
telegram: isSmallScreen ? 20 : 24,
instagram: isSmallScreen ? 18 : 22,
facebook: isSmallScreen ? 22 : 26,
bell: isSmallScreen ? 20 : 24,
}),
[],
);
const openTelegram = useCallback(async () => {
try {
await AppLink.maybeOpenURL('tg://resolve?domain=cpostuz', {
appName: 'Telegram',
appStoreId: 686449807,
appStoreLocale: 'us',
playStoreId: 'org.telegram.messenger',
});
} catch (err) {
// Agar ilovani ham, storeni ham ochib bolmasa, fallback URL
Linking.openURL('https://t.me/cpostuz');
}
}, []);
const openInstagram = useCallback(async () => {
try {
await AppLink.maybeOpenURL('instagram://user?username=cpost_cargo', {
appName: 'Instagram',
appStoreId: 389801252,
appStoreLocale: 'us',
playStoreId: 'com.instagram.android',
});
} catch (err) {
// Agar ilovani ham, storeni ham ochib bolmasa, fallback URL
Linking.openURL('instagram://user?username=cpost_cargo');
}
}, []);
const openFacebook = useCallback(async () => {
try {
await AppLink.maybeOpenURL('fb://user?username=cpost_cargo', {
appName: 'Facebook',
appStoreId: 284882215,
appStoreLocale: 'us',
playStoreId: 'com.facebook.katana',
});
} catch (err) {
Linking.openURL('https://facebook.com/');
}
}, []);
const handleCloseBrowser = useCallback(() => {
setModalVisible(false);
}, []);
return (
<>
<View style={styles.header}>
<View style={styles.logo}>
<Image source={Logo} style={styles.logoImage} />
<Text style={styles.title}>CPOST</Text>
</View>
<View style={styles.links}>
<TouchableOpacity onPress={openTelegram}>
<Fontisto name="telegram" color="#fff" size={iconSizes.telegram} />
</TouchableOpacity>
<TouchableOpacity onPress={openInstagram}>
<Fontisto
name="instagram"
color="#fff"
size={iconSizes.instagram}
/>
</TouchableOpacity>
{/* <TouchableOpacity onPress={openFacebook}>
<MaterialIcons
name="facebook"
color="#fff"
size={iconSizes.facebook}
/>
</TouchableOpacity> */}
{Platform.OS === 'android' && (
<TouchableOpacity
onPress={() => navigation.navigate('Notifications')}
>
<MaterialCommunityIcons
name="bell-outline"
color="#fff"
size={iconSizes.bell}
/>
{/* <View style={styles.bellDot} /> */}
</TouchableOpacity>
)}
</View>
</View>
<InAppBrowser
visible={modalVisible}
url={browserUrl}
onClose={handleCloseBrowser}
/>
</>
);
};
const styles = StyleSheet.create({
header: {
backgroundColor: '#28A7E8',
height: 80,
paddingHorizontal: 10,
paddingVertical: 10,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
title: {
color: '#fff',
fontSize: 20,
fontWeight: 'bold',
},
logo: {
flexDirection: 'row',
alignItems: 'center',
gap: 5,
},
logoImage: {
width: 40,
height: 40,
resizeMode: 'contain',
},
links: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
},
bellDot: {
width: 10,
height: 10,
position: 'absolute',
backgroundColor: 'red',
right: 2,
borderRadius: 100,
},
});
export default Navbar;

View File

@@ -0,0 +1,40 @@
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import * as React from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import ArrowLeft from 'svg/ArrowLeft';
interface NavbarBackProps {
title: string;
}
const NavbarBack = ({ title }: NavbarBackProps) => {
const navigation = useNavigation<NativeStackNavigationProp<any>>();
return (
<View style={styles.header}>
<TouchableOpacity onPress={() => navigation.goBack()}>
<ArrowLeft color="#fff" width={20} height={20} />
</TouchableOpacity>
<Text style={styles.headerTitle}>{title}</Text>
</View>
);
};
export default NavbarBack;
const styles = StyleSheet.create({
headerWrap: { flex: 1 },
header: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#28A7E8',
height: 60,
paddingHorizontal: 12,
},
headerTitle: {
color: '#fff',
fontSize: 20,
fontWeight: '600',
marginLeft: 8,
},
});

View File

@@ -0,0 +1,119 @@
import { useNavigation, useRoute } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import React, { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import {
Dimensions,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import HomeIcon from 'svg/HomeIcon';
import Passport from 'svg/Passport';
import StatusIcon from 'svg/StatusIcon';
import User from 'svg/User';
import Wallet from 'svg/Wallet';
const { width } = Dimensions.get('window');
const isSmallScreen = width < 360;
type RouteName = 'Home' | 'Status' | 'Passports' | 'Wallet' | 'Profile';
const Navigation = () => {
const navigation = useNavigation<NativeStackNavigationProp<any>>();
const route = useRoute();
const { t } = useTranslation();
const links = useMemo(
() => [
{ label: 'Asosiy', icon: HomeIcon, route: 'Home' as RouteName },
{ label: 'Status', icon: StatusIcon, route: 'Status' as RouteName },
{ label: 'Passportlar', icon: Passport, route: 'Passports' as RouteName },
{ label: "To'lov", icon: Wallet, route: 'Wallet' as RouteName },
{ label: 'Profil', icon: User, route: 'Profile' as RouteName },
],
[],
);
const handleNavigation = useCallback(
(routeName: RouteName) => {
if (route.name === routeName) return;
navigation.replace(routeName);
},
[navigation, route.name],
);
const containerStyle = useMemo(() => [styles.container], []);
const renderNavItem = useCallback(
({ label, icon: Icon, route: routeName }: any) => {
const isActive = route.name === routeName;
const color = isActive ? '#28A7E8' : '#6C6C6C';
const iconSize = isSmallScreen ? 24 : 35;
const fontSize = isSmallScreen ? 10 : 10;
return (
<TouchableOpacity
key={routeName}
style={styles.links}
onPress={() => handleNavigation(routeName)}
activeOpacity={0.7}
>
<Icon color={color} width={iconSize} height={iconSize} />
<Text
style={{
color,
fontWeight: '500',
fontSize,
}}
>
{t(label)}
</Text>
</TouchableOpacity>
);
},
[route.name, handleNavigation, t],
);
const navItems = useMemo(
() => links.map(renderNavItem),
[links, renderNavItem],
);
return (
<View style={styles.wrapper}>
<View style={containerStyle}>{navItems}</View>
</View>
);
};
const styles = StyleSheet.create({
wrapper: {
bottom: 0,
width: '100%',
height: 60,
},
container: {
height: '100%',
backgroundColor: '#fff',
borderTopLeftRadius: 30,
borderTopRightRadius: 30,
flexDirection: 'row',
shadowColor: '#000',
shadowOpacity: 0.1,
justifyContent: 'space-around',
shadowOffset: { width: 0, height: -2 },
shadowRadius: 10,
elevation: 10,
width: '100%',
},
links: {
justifyContent: 'center',
alignItems: 'center',
},
});
export default Navigation;

View File

@@ -0,0 +1,24 @@
import { createNavigationContainerRef } from '@react-navigation/native';
import { RootStackParamList } from 'types/types';
export const navigationRef = createNavigationContainerRef<RootStackParamList>();
// Overloads
export function navigate<RouteName extends keyof RootStackParamList>(
name: RouteName,
): void;
export function navigate<RouteName extends keyof RootStackParamList>(
name: RouteName,
params: RootStackParamList[RouteName],
): void;
// Implementation
export function navigate<RouteName extends keyof RootStackParamList>(
name: RouteName,
params?: RootStackParamList[RouteName],
) {
if (navigationRef.isReady()) {
// @ts-expect-error — bu yerda overload tiplari bilan moslashadi
navigationRef.navigate(name, params);
}
}

View File

@@ -0,0 +1,75 @@
import LottieView from 'lottie-react-native';
import React, { useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import {
Animated,
StyleSheet,
Text,
useWindowDimensions,
View,
} from 'react-native';
import ProgressBar from 'screens/../../assets/lottie/non data found.json';
interface Props {
message?: string;
progress?: number;
}
const NoResult: React.FC<Props> = ({
message = "Hech qanday ma'lumot topilmadi",
progress = 0,
}) => {
const { width } = useWindowDimensions();
const { t } = useTranslation();
const TOTAL_FRAMES = 90;
const scale = width < 360 ? 0.8 : 1;
const translateX = useRef(new Animated.Value(-width)).current;
const progressBarRef = useRef<LottieView>(null);
useEffect(() => {
Animated.loop(
Animated.sequence([
Animated.timing(translateX, {
toValue: width / 2 + 75 * scale,
duration: 6000,
useNativeDriver: true,
}),
Animated.timing(translateX, {
toValue: -width / 2 - 100 * scale,
duration: 0,
useNativeDriver: true,
}),
]),
).start();
}, [translateX, width, scale]);
useEffect(() => {
if (progressBarRef.current) {
progressBarRef.current?.play(0, Math.floor(progress * TOTAL_FRAMES));
}
}, [progress]);
return (
<View style={styles.container}>
<LottieView
source={ProgressBar}
loop
autoPlay={true}
resizeMode="cover"
style={{ width: 150 * scale, height: 150 * scale }}
/>
<Text>{t(message)}</Text>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
overflow: 'hidden',
},
});
export default NoResult;

View File

@@ -0,0 +1,131 @@
// components/Pagination.tsx
import React from 'react';
import { Text, TouchableOpacity, View } from 'react-native';
import ArrowLeft from 'svg/ArrowLeft';
import ArrowRightUnderline from 'svg/ArrowRightUnderline';
interface PaginationProps {
page: number; // current page (0-based)
totalPages: number;
setPage: (page: number) => void;
}
const Pagination: React.FC<PaginationProps> = ({
page,
totalPages,
setPage,
}) => {
if (totalPages <= 1) return null;
const renderPages = () => {
const pages: (number | string)[] = [];
// doim 1-sahifa
pages.push(0);
// agar current sahifa 4 dan katta bolsa ... qoshamiz
if (page > 3) {
pages.push('...');
}
// current page atrofidagi 2 ta sahifani korsatamiz
for (
let p = Math.max(1, page - 1);
p <= Math.min(page + 1, totalPages - 2);
p++
) {
if (p !== 0 && p !== totalPages - 1) {
pages.push(p);
}
}
// agar current sahifa oxiriga yaqin bolmasa, ... qoshamiz
if (page < totalPages - 4) {
pages.push('...');
}
// doim oxirgi sahifa
if (totalPages > 1) {
pages.push(totalPages - 1);
}
return pages.map((p, index) => {
if (p === '...') {
return (
<Text key={`dots-${index}`} style={{ marginHorizontal: 5 }}>
...
</Text>
);
}
return (
<TouchableOpacity
key={p}
onPress={() => setPage(p as number)}
style={{
backgroundColor: page === p ? '#28A7E8' : '#ccc',
width: 30,
height: 30,
borderRadius: 8,
justifyContent: 'center',
alignItems: 'center',
marginHorizontal: 5,
}}
>
<Text style={{ color: page === p ? '#fff' : '#000' }}>
{(p as number) + 1}
</Text>
</TouchableOpacity>
);
});
};
return (
<View
style={{
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
paddingVertical: 10,
}}
>
{/* prev button */}
<TouchableOpacity
disabled={page === 0}
onPress={() => setPage(page - 1)}
style={{
backgroundColor: page === 0 ? '#aaa' : '#28A7E8',
width: 30,
height: 30,
borderRadius: 8,
justifyContent: 'center',
alignItems: 'center',
marginHorizontal: 5,
}}
>
<ArrowLeft color="#fff" />
</TouchableOpacity>
{/* pages */}
{renderPages()}
{/* next button */}
<TouchableOpacity
disabled={page >= totalPages - 1}
onPress={() => setPage(page + 1)}
style={{
backgroundColor: page >= totalPages - 1 ? '#aaa' : '#28A7E8',
width: 30,
height: 30,
borderRadius: 8,
justifyContent: 'center',
alignItems: 'center',
marginHorizontal: 5,
}}
>
<ArrowRightUnderline color="#fff" />
</TouchableOpacity>
</View>
);
};
export default Pagination;

View File

@@ -0,0 +1,38 @@
// src/components/PatternPad.tsx
import React from 'react';
import { StyleSheet, View } from 'react-native';
import GesturePassword from 'react-native-gesture-password';
type Props = {
status?: 'normal' | 'right' | 'wrong';
message?: string;
onFinish: (pattern: string) => void; // masalan: "1-2-5-8"
};
export default function PatternPad({
status = 'normal',
message = 'Draw pattern',
onFinish,
}: Props) {
return (
<View style={styles.wrap}>
<GesturePassword
interval={8}
outerCircle
allowCross
status={status}
message={message}
onEnd={pwd => onFinish(pwd)}
/>
</View>
);
}
const styles = StyleSheet.create({
wrap: {
width: '100%',
height: 360,
justifyContent: 'center',
alignItems: 'center',
},
});

View File

@@ -0,0 +1,201 @@
import { useCallback, useEffect, useMemo, useRef } from 'react';
import {
Animated,
Image,
StyleSheet,
Text,
useWindowDimensions,
View,
} from 'react-native';
import Logo from 'screens/../../assets/bootsplash/logo.png';
const SplashScreen = ({ onFinish }: { onFinish: () => void }) => {
const { width: screenWidth, height: screenHeight } = useWindowDimensions();
const circleSize = useMemo(
() => Math.min(Math.max(screenWidth * 0.6, 150), 300),
[screenWidth],
);
const fadeAnim = useRef(new Animated.Value(0)).current;
const scaleAnim = useRef(new Animated.Value(0.5)).current;
const slideAnim = useRef(new Animated.Value(0)).current;
const animationConfig = useMemo(
() => ({
fadeAnimation: Animated.timing(fadeAnim, {
toValue: 1,
duration: 1000,
useNativeDriver: true,
}),
scaleAnimation: Animated.spring(scaleAnim, {
toValue: 1,
tension: 50,
friction: 7,
useNativeDriver: true,
}),
slideAnimation: Animated.timing(slideAnim, {
toValue: -screenWidth,
duration: 500,
useNativeDriver: true,
}),
}),
[fadeAnim, scaleAnim, slideAnim, screenWidth],
);
const startInitialAnimations = useCallback(() => {
Animated.parallel([
animationConfig.fadeAnimation,
animationConfig.scaleAnimation,
]).start();
}, [animationConfig]);
const startSlideAnimation = useCallback(() => {
animationConfig.slideAnimation.start(() => onFinish());
}, [animationConfig, onFinish]);
useEffect(() => {
startInitialAnimations();
const timeoutId = setTimeout(() => {
startSlideAnimation();
}, 2000);
return () => clearTimeout(timeoutId);
}, [startInitialAnimations, startSlideAnimation]);
const circleStyles = useMemo(
() => ({
outer: {
width: screenWidth * 1.5,
height: screenWidth * 1.5,
borderRadius: (screenWidth * 1.5) / 2,
},
middle: {
width: screenWidth * 1.1,
height: screenWidth * 1.1,
borderRadius: (screenWidth * 1.1) / 2,
},
inner: {
width: circleSize,
height: circleSize,
borderRadius: circleSize / 2,
},
}),
[screenWidth, circleSize],
);
const logoStyles = useMemo(
() => ({
width: circleSize * 0.8,
height: circleSize * 0.8,
top: -circleSize * 0.25,
}),
[circleSize],
);
const brandTextStyles = useMemo(
() => ({
bottom: -circleSize * 0.1,
}),
[circleSize],
);
const brandFontSize = useMemo(
() => Math.min(Math.max(screenWidth * 0.15, 24), 60),
[screenWidth],
);
const containerStyle = useMemo(
() => [styles.container, { transform: [{ translateX: slideAnim }] }],
[slideAnim],
);
const logoContainerStyle = useMemo(
() => [
styles.logoContainer,
{
opacity: fadeAnim,
transform: [{ scale: scaleAnim }],
},
],
[fadeAnim, scaleAnim],
);
return (
<Animated.View style={containerStyle}>
<View style={styles.gradientBackground} />
<Animated.View style={logoContainerStyle}>
<View style={styles.circleWrapper}>
<View style={[styles.circleOuter, circleStyles.outer]} />
<View style={[styles.circleMiddle, circleStyles.middle]} />
<View style={[styles.circleInner, circleStyles.inner]}>
<Image
source={Logo}
style={[styles.logo, logoStyles]}
resizeMode="contain"
/>
<View style={[styles.brandText, brandTextStyles]}>
<Text style={[styles.brand, { fontSize: brandFontSize }]}>
CPOST
</Text>
</View>
</View>
</View>
</Animated.View>
</Animated.View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
position: 'absolute',
width: '100%',
height: '100%',
zIndex: 1000,
},
gradientBackground: {
...StyleSheet.absoluteFillObject,
backgroundColor: '#27A7E8',
},
logoContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
circleWrapper: {
justifyContent: 'center',
alignItems: 'center',
},
circleOuter: {
backgroundColor: 'rgba(255, 255, 255, 0.1)',
position: 'absolute',
},
circleMiddle: {
backgroundColor: 'rgba(255, 255, 255, 0.1)',
position: 'absolute',
},
circleInner: {
backgroundColor: 'rgba(255, 255, 255, 0.1)',
justifyContent: 'center',
alignItems: 'center',
position: 'relative',
overflow: 'visible',
},
logo: {
position: 'absolute',
alignSelf: 'center',
},
brandText: {
position: 'absolute',
width: '100%',
alignItems: 'center',
},
brand: {
fontWeight: '700',
color: '#fff',
},
});
export default SplashScreen;

10
src/helpers/formatData.ts Normal file
View File

@@ -0,0 +1,10 @@
const formatDate = (dateString: string) => {
if (!dateString) return '';
const date = new Date(dateString);
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0'); // Oy 0-dan boshlanadi
const year = date.getFullYear();
return `${day}/${month}/${year}`;
};
export default formatDate;

View File

@@ -0,0 +1,38 @@
/**
* Format the number (+998 00 111-22-33)
* @param value Number to be formatted (XXXYYZZZAABB)
* @returns string +998 00 111-22-33
*/
const formatPhone = (value: string) => {
// Keep only numbers
const digits = value.replace(/\D/g, '');
// Return empty string if data is not available
if (digits.length === 0) {
return '';
}
const prefix = digits.startsWith('998') ? '+998 ' : '+998 ';
let formattedNumber = prefix;
if (digits.length > 3) {
formattedNumber += digits.slice(3, 5);
}
if (digits.length > 5) {
formattedNumber += ' ' + digits.slice(5, 8);
}
if (digits.length > 8) {
formattedNumber += '-' + digits.slice(8, 10);
}
if (digits.length > 10) {
formattedNumber += '-' + digits.slice(10, 12);
}
return formattedNumber.trim();
};
export default formatPhone;

View File

@@ -0,0 +1,7 @@
const getWeekday = (dateStr: string) => {
const date = new Date(dateStr);
const days = ["Ya", "Du", "Se", "Cho", "Pa", "Ju", "Sha"];
return days[date.getDay()];
};
export default getWeekday

40
src/i18n/i18n.ts Normal file
View File

@@ -0,0 +1,40 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import * as RNLocalize from 'react-native-localize';
import uz from './locales/uz.json';
import ru from './locales/ru.json';
type TranslationResources = {
[key: string]: {
translation: Record<string, string>;
};
};
const resources: TranslationResources = {
uz: { translation: uz },
ru: { translation: ru },
};
const languageDetector = {
type: 'languageDetector',
async: true,
detect: (callback: (lang: string) => void) => {
const bestLang = RNLocalize.findBestLanguageTag(['uz', 'ru']);
callback(bestLang?.languageTag ?? 'uz');
},
init: () => { },
cacheUserLanguage: () => { },
};
i18n
.use(languageDetector as any)
.use(initReactI18next)
.init({
fallbackLng: 'uz',
resources,
interpolation: {
escapeValue: false,
},
});
export default i18n;

4
src/i18n/locales/en.json Normal file
View File

@@ -0,0 +1,4 @@
{
"hello": "Hello",
"select_language": "Select Language"
}

229
src/i18n/locales/ru.json Normal file
View File

@@ -0,0 +1,229 @@
{
"hello": "Здравствуйте",
"select_language": "Выберите язык",
"Royxatdan otganmisz": "Вы зарегистрированы?",
"Botdan ro'yxatdan otganmisiz": "Вы зарегистрированы через бота?",
"Tizimga kirish": "Войти в систему",
"Yangi royxatdan o'tmoqchimisiz": "Хотите зарегистрироваться?",
"Royxatdan otish": "Регистрация",
"Telefon raqami": "Номер телефона",
"Passport seriya raqami": "Серия и номер паспорта",
"Filial": "Филиал",
"Filialni tanlang...": "Выберите филиал...",
"ID va kabinet yoqmi?": "Нет ID и кабинета?",
"Xato raqam kiritildi": "Введен неправильный номер",
"2 ta harf kerak": "Нужно 2 буквы",
"7 ta raqam kerak": "Нужно 7 цифр",
"Filialni tanlang": "Выберите филиал",
"Ro'yxatdan o'tish": "Регистрация",
"Ism": "Имя",
"Ismingiz": "Ваше имя",
"Familiya": "Фамилия",
"Familiyangiz": "Ваша фамилия",
"Bizni qaerdan topdingiz?": "Как вы нас нашли?",
"Bizni kim tavsiya qildi...": "Кто вас порекомендовал...",
"Foydalanish shartlari": "Условия использования",
"bilan tanishib chiqdim!": "ознакомлен!",
"Davom etish": "Продолжить",
"Majburiy maydon": "Обязательное поле",
"Eng kamida 3ta belgi bo'lishi kerak": "Минимум 3 символа",
"Tanishim orqali": "Через знакомого",
"Telegram orqali": "Через Telegram",
"Instagram orqali": "Через Instagram",
"Facebook orqali": "Через Facebook",
"foydalanish_shartlari_va_qoidalari": "Условия и правила использования",
"umumiy_qoidalar": "1. Общие положения",
"umumiy_qoidalar_text": "Настоящие условия использования (далее «Условия») регулируют ваше использование данного приложения. Используя приложение, вы полностью принимаете эти условия.",
"foydalanuvchi_majburiyatlari": "2. Обязанности пользователя",
"foydalanuvchi_majburiyatlari_text": "• Предоставлять достоверную и точную информацию\n• Уважать права других пользователей\n• Не использовать систему в недобросовестных целях\n• Соблюдать правила безопасности",
"maxfiylik_siyosati": "3. Политика конфиденциальности",
"maxfiylik_siyosati_text": "Ваши персональные данные защищаются в соответствии с нашей политикой конфиденциальности. Мы не передаем ваши данные третьим лицам и обеспечиваем их безопасность.",
"javobgarlik": "4. Ответственность",
"javobgarlik_text": "Компания не несет ответственности за возможные убытки, возникшие в результате использования приложения. Пользователь несет полную ответственность за свои действия.",
"shartlarni_ozgartirish": "5. Изменение условий",
"shartlarni_ozgartirish_text": "Компания оставляет за собой право изменять настоящие условия в любое время. Изменения будут опубликованы в приложении с указанием даты вступления в силу.",
"aloqa": "6. Контакты",
"aloqa_text": "Если у вас есть вопросы или предложения, свяжитесь с нами по следующему адресу:\nEmail: support@company.uz\nТелефон: +998 71 123 45 67",
"oxirgi_yangilanish": "Последнее обновление:",
"roziman": "Согласен",
"Shaxsiy maʼlumotlar": "Личные данные",
"JSHSHIR": "ИНП",
"Tug'ilgan sana": "Дата рождения",
"Passport/ID karta rasmi yoki faylni yuklang": "Загрузите фото паспорта/ID карты или файл",
"Old tomon": "Лицевая сторона",
"Orqa tomon": "Обратная сторона",
"Tasdiqlash": "Подтвердить",
"14 ta raqam kerak": "Нужно 14 цифр",
"Faylni yuklang": "Загрузите файл",
"Tasdiqlash kodini kiriting": "Введите код подтверждения",
"raqamiga yuborilgan": "отправлено на номер",
"xonali kodni kiriting.": "введите код из X цифр.",
"Kod yuborildi": "Код отправлен",
"Tasdiqlash kodi telefon raqamingizga qayta yuborildi.": "Код подтверждения повторно отправлен на ваш номер.",
"Kod tasdiqlandi": "Код подтвержден",
"Sizning ID raqamingiz:": "Ваш ID номер:",
"Xato": "Ошибка",
"Iltimos, to'liq tasdiqlash kodini kiriting.": "Пожалуйста, введите полный код подтверждения.",
"Kodni qayta yuborish": "Отправить код повторно",
"Kodni qayta yuborish vaqti": "Время повторной отправки кода",
"Haqiqatan ham profildan chiqmoqchimisiz?": "Вы уверены, что хотите выйти из профиля?",
"Ya": "Вс",
"Du": "Пн",
"Se": "Вт",
"Cho": "Ср",
"Faol": "Актив",
"Kutilmoqda": "Ожидание",
"Faol emas": "Неактивно",
"Xatolik yuz berdi!": "Произошла ошибка!",
"Akkaunt faol emas!": "Аккаунт неактивен!",
"Avto kargoda bir maxsulotdan seriyalab istalgan katta miqdorda olish mumkin. Doimiykop yuk oluvchi ijozlar uchun maxsus arzonlashtrilgan narxlarimiz bor": "Вы можете получить любую большую сумму от одного товара серийно в автопогрузке. У нас есть специальные сниженные цены для разрешений на постоянные большие грузы.",
"Pa": "Чт",
"Bekor qilish": "Отменить",
"Ju": "Пт",
"Sha": "Сб",
"Yetkazish tafsilotlari": "Детали доставки",
"Buyurtmani qadoqlash kunlari": "Дни упаковки заказа",
"Boshlanish": "Начало",
"Tugatish": "Окончание",
"IN_CUSTOMS": "В ТАМОЖЕННОЙ",
"COLLECTING": "В СБОРЕ",
"ON_THE_WAY": "В ПУТИ",
"IN_WAREHOUSE": "В СКЛАДЕ",
"Avia orqali yetkazish": "Доставка авиа",
"Avto orqali yetkazish": "Доставка авто",
"Avia post kodi": "Авиапочтовый код",
"Auto post kodi": "Автопочтовый код",
"Nusxa olindi": "Скопировано",
"Pochta kodi nusxalandi!": "Почтовый код скопирован!",
"Kargo narxlari": "Цены на карго",
"Taqiqlangan buyumlar": "Запрещённые товары",
"Filiallar royxati": "Список филиалов",
"Asosiy": "Главный",
"Status": "Статус",
"Passportlar": "Паспорты",
"To'lov": "Оплата",
"Profil": "Профиль",
"Sozlamalar": "Настройки",
"Oddiy maxsulotlar": "Обычные товары",
"(Katta miqdordagi yuklar kelishuv asosida)": "(Крупные партии товаров по договоренности)",
"Brend buyumlar": "Брендовые товары",
"(Karobka,dokumentlar bilan birga)": "(С коробкой и документами)",
"Avia pochtada bir maxsulotdan seriyalab olish mumkin emas": "Нельзя заказывать серийно один и тот же товар авиапочтой",
"Yetib kelish vaqti": "Срок доставки",
"kun": "дней",
"Yuklarni yetib elish vaqti oxirgi qadoqlash kunidan boshlab xisoblanadi": "Срок доставки считается с последнего дня упаковки",
"Minimal miqdor talab qilinmaydi.": "Минимальное количество не требуется.",
"Muhim!": "Важно!",
"Filiallargacha yetkazib berish - bepul.": "Доставка до филиалов - бесплатно.",
"Batafsil": "Подробнее",
"Filiallarimiz ro'yhati ilovada mavjud": "Список наших филиалов доступен в приложении",
"Agar siz yashayotgan hududda bizning filialimiz mavjud bo'lmasa, o'zingizga eng yaqin bo'lgan filialni tanlab, ro'yhatdan o'tishingiz mumkin.": "Если в вашем регионе нет нашего филиала, вы можете выбрать ближайший и пройти регистрацию.",
"ya'ni, qancha gramm mahsulot olsangiz, shuncha og'irligi (gramm) uchun to'lov qilasiz.": "То есть, вы платите за каждый грамм товара, который вы заказываете.",
"Avto kargoda bir mahsulotdan seriyali ravishda, istalgan katta miqdorda xarid qilish mumkin. Doimiy ravishda kop yuk oluvchilar uchun maxsus chegirmali narxlarimiz mavjud.": "В авто-карго можно приобрести один и тот же товар серийно, в любом большом количестве. Для постоянных крупных заказчиков у нас есть специальные скидки.",
"dan boshlab": "от",
"Aviada taqiqlangan buyumlar": "Запрещенные товары для авиа-доставки",
"Ichida suyuqligi bor narsalar": "Предметы с жидкостями внутри",
"Batareykasi va magnit bolgan istalgan narsa": "Любые предметы с батареей и магнитом",
"(Telifon, sensitive buyumlar, airpods, naushnik, qol soati, tagi yonadigan krasovkalar...)": "(Телефоны, чувствительные устройства, airpods, наушники, наручные часы, кроссовки с подсветкой...)",
"Kukunli buyumlar": "Порошкообразные вещества",
"(Pudra, ten...)": "(Пудра, тени...)",
"Parfumeriya": "Парфюмерия",
"(Barcha Parfumeriya va kosmetika, yuvinish maxsulotlari)": "(Вся парфюмерия и косметика, средства для мытья)",
"Otkir tigli va sovuq qirollar": "Острые и холодное оружие",
"(Pichoq, qaychi, miltiq...)": "(Ножи, ножницы, ружья...)",
"Zargarklik buyumlari": "Ювелирные изделия",
"(Tilla, kumush, olmos, braslit...)": "(Золото, серебро, бриллианты, браслеты...)",
"Oziq ovqat": "Продукты питания",
"Manzilni tekshirish": "Проверить адрес",
"Dron, avtomat qurollar": "Дрон, автоматическое оружие",
"Diniy kitob va diniy buyumlar": "Религиозные книги и религиозные предметы",
"Agar sizda g'ayrioddiy yoki noaniq mahsulot bo'lsa, albatta buyurtma qilishdan oldin so'rashingiz tavsiya etiladi.": "Если у вас есть необычный или непонятный товар, рекомендуется обязательно уточнить перед заказом.",
"Dori darmon va med texnika": "Лекарства и медицинское оборудование",
"Avia pochta manzili orqali yuborilishi taqiqlangan mahsulot buyurtma qilgan bo'lsangiz, u avtomatik ravishda avtokargo yukiga (avto) o'tkaziladi. Shunday qilib, yukingiz Xitoy omborida qolib ketmaydi.": "Если вы заказали товар, запрещённый к отправке авиа-почтой, он автоматически будет перенаправлен на авто-карго. Таким образом, ваша посылка не останется на складе в Китае.",
"Shu bilan birga, Aviada ham, Avtoda ham taqiqlangan mahsulot yuborilgan bo'lsa bunday holatda mahsulot O'zbekistonga yuborilmaydi va bu uchun javobgarlik mijozga yuklanadi.": "Если запрещённый товар был отправлен как авиа, так и авто-доставкой, то в таком случае товар не будет отправлен в Узбекистан, и ответственность ложится на клиента.",
"(Bu kargo narxini osishiga olib keladi. Seriyali buyularni avto kargo orqali olib kelish arzonga tushadi)": "(Это увеличивает стоимость доставки. Серийные товары дешевле доставлять автокарго)",
"Manzil": "Адрес",
"Ish vaqti": "Время работы",
"Telefon": "Телефон",
"Telegram admin": "Админ Telegram",
"Telegram kanal": "Канал Telegram",
"Barchasi": "Все",
"Yig'ilmoqda": "Собирается",
"Yo'lda": "В пути",
"Bojxonada": "На таможне",
"Toshkent omboriga yetib keldi": "Прибыла на Ташкентский склад",
"Topshirish punktiga yuborildi": "Направлено в пункт сдачи",
"Qabul qilingan": "Принято",
"Filter": "Фильтр",
"Transport": "Транспорт",
"Reys raqami": "Номер рейса",
"Avto": "Авто",
"Avia": "Авиа",
"ID orqali izlash": "Поиск по ID",
"Mahsulotlar ogirligi": "Вес товаров",
"Umumiy narxi": "Общая цена",
"Buyurtmalar soni": "Количество заказов",
"Mahsulotlar": "Продукты",
"Ogirligi": "Вес",
"Trek ID": "Трек ID",
"Narxi": "Цена",
"som": "сум",
"Umumiy narx": "Общая цена",
"Yopish": "Закрыть",
"Passportlarim": "Мои паспорта",
"Hali pasport qo'shilmagan": "Паспорт еще не добавлен",
"Yangi pasport qo'shish uchun tugmani bosing": "Нажмите кнопку, чтобы добавить новый паспорт",
"Yangi pasport qo'shish": "Добавить новый паспорт",
"Passport malumotlarim": "Мои паспортные данные",
"Tez ID": "Быстрый ID",
"Toliq ismi": "Полное имя",
"Passport seriya": "Серия паспорта",
"Tugilgan kun": "Дата рождения",
"Limit": "Лимит",
"Qo'shish": "Добавить",
"Passport muvaffaqqiyatli qo'shildi": "Паспорт успешно добавлен",
"Yaxshi": "Хорошо",
"To'langan": "Оплачено",
"To'lanmagan": "Неоплачено",
"Mahsulotlar soni": "Количество товаров",
"To'lash": "Оплата",
"Yetkazish vaqti": "Время доставки",
"To'lov usuli": "Способ оплаты",
"Bank kartasi": "Банковская карта",
"Naqt pul": "Наличные",
"Tolov muvaffaqqiyatli otdi": "Платёж прошёл успешно",
"Chop etilmoqda": "Печатается",
"Toʻlovingiz tasdiqlandi!": "Ваш платёж подтверждён!",
"Iltimos ozroq kutib turing!": "Пожалуйста, подождите немного!",
"Karta nomi": "Название карты",
"Karta raqami": "Номер карты",
"Muddati": "Срок действия",
"ilovasi orqali to'lash": "оплатить через приложение",
"Karta raqami xato": "Неверный номер карты",
"Amal qilish muddati xato": "Неверный срок действия",
"Nusxa olingan": "Скопировано",
"Avto manzili nusxalandi!": "Автоадрес успешно скопирован!",
"Avia manzili nusxalandi!": "Авиа адрес скопирован",
"Rasmni o'zgartirish": "Изменить изображение",
"Rasmni o'chirish": "Удалить изображение",
"Bildirishnomalar": "Уведомления",
"Xitoy omborlari manzili": "Адреса складов в Китае",
"Yordam markazi": "Центр помощи",
"Chiqish": "Выйти",
"Hisobingizni ochirish": "Удалить аккаунт",
"Bizning Xitoy manzilimiz": "Наш китайский адрес",
"Taobao, pinduoduo, 1688 ,alibaba va Xitoyning istalgan platformasiga kiritish uchun": "Для ввода на Taobao, Pinduoduo, 1688, Alibaba и любые другие китайские платформы",
"Xitoy omborlarimiz manzilini programmaga kiriting": "Введите адрес наших китайских складов в приложение",
"Diqqat! Iltimos, Xitoy omborimiz manzilini Xitoy programmalariga kiritganingizdan so'ng, kiritilgan holatdagi skrenshotni bizga yuborib, tekshirtiring": "Внимание! Пожалуйста, после ввода адреса нашего китайского склада в китайские приложения, отправьте нам скриншот для проверки",
"Xitoy ombori manzilini to'g'ri kiritish, mahsulotingiz yo'qolib qolish oldini oladi.": "Правильный ввод адреса китайского склада предотвратит потерю вашего товара.",
"Agar sizda savol tug'ilsa yoki biron narsaga tushunmasangiz bizga murojaat qiling": "Если у вас возникнут вопросы или что-то будет непонятно — свяжитесь с нами",
"Skrenshot rasmini yuklang": "Загрузите скриншот",
"Rasmni shu yerga yuklang": "Загрузите изображение сюда",
"Yordam xizmati": "Служба поддержки",
"Xabar yozing...": "Напишите сообщение...",
"Yuborish": "Отправить",
"Xatolik yuz berdi": "Произошла ошибка",
"Passport qo'shishda xatolik yuz berdi": "Ошибка добавления паспорта",
"Kodsiz tovarlar": "Товары без кода",
"Hech qanday ma'lumot topilmadi": "Нет данных"
}

230
src/i18n/locales/uz.json Normal file
View File

@@ -0,0 +1,230 @@
{
"hello": "Salom",
"select_language": "Tilni tanlang",
"Royxatdan otganmisz": "Royxatdan otganmisz",
"Botdan ro'yxatdan otganmisiz": "Botdan ro'yxatdan otganmisiz",
"Tizimga kirish": "Tizimga kirish",
"Yangi royxatdan o'tmoqchimisiz": "Yangi royxatdan o'tmoqchimisiz",
"Royxatdan otish": "Royxatdan otish",
"Telefon raqami": "Telefon raqami",
"Passport seriya raqami": "Passport seriya raqami",
"Filial": "Filial",
"Filialni tanlang...": "Filialni tanlang...",
"ID va kabinet yoqmi?": "ID va kabinet yoqmi?",
"Xato raqam kiritildi": "Xato raqam kiritildi",
"2 ta harf kerak": "2 ta harf kerak",
"7 ta raqam kerak": "7 ta raqam kerak",
"Filialni tanlang": "Filialni tanlang",
"Ro'yxatdan o'tish": "Ro'yxatdan o'tish",
"Ism": "Ism",
"Ismingiz": "Ismingiz",
"Familiya": "Familiya",
"Familiyangiz": "Familiyangiz",
"Bizni qaerdan topdingiz?": "Bizni qaerdan topdingiz?",
"Bizni kim tavsiya qildi...": "Bizni kim tavsiya qildi...",
"Foydalanish shartlari": "Foydalanish shartlari",
"bilan tanishib chiqdim!": "bilan tanishib chiqdim!",
"Davom etish": "Davom etish",
"Majburiy maydon": "Majburiy maydon",
"Eng kamida 3ta belgi bo'lishi kerak": "Eng kamida 3ta belgi bo'lishi kerak",
"Tanishim orqali": "Tanishim orqali",
"Telegram orqali": "Telegram orqali",
"Instagram orqali": "Instagram orqali",
"Facebook orqali": "Facebook orqali",
"foydalanish_shartlari_va_qoidalari": "Foydalanish shartlari va qoidalari",
"umumiy_qoidalar": "1. Umumiy qoidalar",
"umumiy_qoidalar_text": "Ushbu foydalanish shartlari (keyingi o'rinlarda \"Shartlar\") sizning ushbu ilovadan foydalanishingizni tartibga soladi. Ilovadan foydalanish orqali siz ushbu shartlarni to'liq qabul qilasiz.",
"foydalanuvchi_majburiyatlari": "2. Foydalanuvchi majburiyatlari",
"foydalanuvchi_majburiyatlari_text": "• To'g'ri va aniq ma'lumotlar taqdim etish\n• Boshqa foydalanuvchilarning huquqlarini hurmat qilish\n• Tizimdan noto'g'ri maqsadlarda foydalanmaslik\n• Xavfsizlik qoidalariga rioya qilish",
"maxfiylik_siyosati": "3. Maxfiylik siyosati",
"maxfiylik_siyosati_text": "Sizning shaxsiy ma'lumotlaringiz maxfiylik siyosatimizga muvofiq himoyalanadi. Biz sizning ma'lumotlaringizni uchinchi shaxslarga bermaydi va xavfsiz saqlashni ta'minlaymiz.",
"javobgarlik": "4. Javobgarlik",
"javobgarlik_text": "Kompaniya ilovadan foydalanish natijasida yuzaga kelishi mumkin bo'lgan zararlar uchun javobgar emas. Foydalanuvchi o'z harakatlari uchun to'liq javobgarlikni o'z zimmasiga oladi.",
"shartlarni_ozgartirish": "5. Shartlarni o'zgartirish",
"shartlarni_ozgartirish_text": "Kompaniya ushbu shartlarni istalgan vaqtda o'zgartirish huquqini o'zida saqlab qoladi. O'zgarishlar ilovada e'lon qilinadi va kuchga kirish sanasi ko'rsatiladi.",
"aloqa": "6. Aloqa",
"aloqa_text": "Savollar yoki takliflar bo'lsa, biz bilan quyidagi manzil orqali bog'laning:\nEmail: support@company.uz\nTelefon: +998 71 123 45 67",
"oxirgi_yangilanish": "Oxirgi yangilanish:",
"roziman": "Roziman",
"Shaxsiy maʼlumotlar": "Shaxsiy maʼlumotlar",
"JSHSHIR": "JSHSHIR",
"Tug'ilgan sana": "Tug'ilgan sana",
"Passport/ID karta rasmi yoki faylni yuklang": "Passport/ID karta rasmi yoki faylni yuklang",
"Old tomon": "Old tomon",
"Orqa tomon": "Orqa tomon",
"Tasdiqlash": "Tasdiqlash",
"14 ta raqam kerak": "14 ta raqam kerak",
"Faylni yuklang": "Faylni yuklang",
"Tasdiqlash kodini kiriting": "Tasdiqlash kodini kiriting",
"raqamiga yuborilgan": "raqamiga yuborilgan",
"xonali kodni kiriting.": "xonali kodni kiriting.",
"Kod yuborildi": "Kod yuborildi",
"Tasdiqlash kodi telefon raqamingizga qayta yuborildi.": "Tasdiqlash kodi telefon raqamingizga qayta yuborildi.",
"Kod tasdiqlandi": "Kod tasdiqlandi",
"Sizning ID raqamingiz:": "Sizning ID raqamingiz:",
"Xato": "Xato",
"Iltimos, to'liq tasdiqlash kodini kiriting.": "Iltimos, to'liq tasdiqlash kodini kiriting.",
"Kodni qayta yuborish": "Kodni qayta yuborish",
"Kodni qayta yuborish vaqti": "Kodni qayta yuborish vaqti",
"Ya": "Ya",
"Du": "Du",
"Se": "Se",
"Cho": "Cho",
"Pa": "Pa",
"Ju": "Ju",
"Sha": "Sha",
"Buyurtmani qadoqlash kunlari": "Buyurtmani qadoqlash kunlari",
"Boshlanish": "Boshlanish",
"Tugatish": "Tugatish",
"Avia orqali yetkazish": "Avia orqali yetkazish",
"Avto orqali yetkazish": "Avto orqali yetkazish",
"Avia post kodi": "Avia post kodi",
"Auto post kodi": "Auto post kodi",
"Nusxa olindi": "Nusxa olindi",
"Pochta kodi nusxalandi!": "Pochta kodi nusxalandi!",
"Kargo narxlari": "Kargo narxlari",
"Taqiqlangan buyumlar": "Taqiqlangan buyumlar",
"Filiallar royxati": "Filiallar royxati",
"Asosiy": "Asosiy",
"Status": "Status",
"Passportlar": "Passportlar",
"To'lov": "To'lov",
"Profil": "Profil",
"Sozlamalar": "Sozlamalar",
"Oddiy maxsulotlar": "Oddiy maxsulotlar",
"(Katta miqdordagi yuklar kelishuv asosida)": "(Katta miqdordagi yuklar kelishuv asosida)",
"Brend buyumlar": "Brend buyumlar",
"(Karobka,dokumentlar bilan birga)": "(Karobka,dokumentlar bilan birga)",
"Avia pochtada bir maxsulotdan seriyalab olish mumkin emas": "Avia pochtada bir maxsulotdan seriyalab olish mumkin emas",
"Yetib kelish vaqti": "Yetib kelish vaqti",
"kun": "kun",
"Yuklarni yetib elish vaqti oxirgi qadoqlash kunidan boshlab xisoblanadi": "Yuklarni yetib elish vaqti oxirgi qadoqlash kunidan boshlab xisoblanadi",
"Minimal miqdor talab qilinmaydi.": "Minimal miqdor talab qilinmaydi.",
"Muhim!": "Muhim!",
"Filiallargacha yetkazib berish - bepul.": "Filiallargacha yetkazib berish - bepul.",
"Batafsil": "Batafsil",
"Filiallarimiz ro'yhati ilovada mavjud": "Filiallarimiz ro'yhati ilovada mavjud",
"Agar siz yashayotgan hududda bizning filialimiz mavjud bo'lmasa, o'zingizga eng yaqin bo'lgan filialni tanlab, ro'yhatdan o'tishingiz mumkin.": "Agar siz yashayotgan hududda bizning filialimiz mavjud bo'lmasa, o'zingizga eng yaqin bo'lgan filialni tanlab, ro'yhatdan o'tishingiz mumkin.",
"ya'ni, qancha gramm mahsulot olsangiz, shuncha og'irligi (gramm) uchun to'lov qilasiz.": "ya'ni, qancha gramm mahsulot olsangiz, shuncha og'irligi (gramm) uchun to'lov qilasiz.",
"(Bu kargo narxini osishiga olib keladi. Seriyali buyularni avto kargo orqali olib kelish arzonga tushadi)": "(Bu kargo narxini osishiga olib keladi. Seriyali buyularni avto kargo orqali olib kelish arzonga tushadi)",
"Avto kargoda bir mahsulotdan seriyali ravishda, istalgan katta miqdorda xarid qilish mumkin. Doimiy ravishda kop yuk oluvchilar uchun maxsus chegirmali narxlarimiz mavjud.": "Avto kargoda bir mahsulotdan seriyali ravishda, istalgan katta miqdorda xarid qilish mumkin. Doimiy ravishda kop yuk oluvchilar uchun maxsus chegirmali narxlarimiz mavjud.",
"dan boshlab": "dan boshlab",
"Aviada taqiqlangan buyumlar": "Aviada taqiqlangan buyumlar",
"Ichida suyuqligi bor narsalar": "Ichida suyuqligi bor narsalar",
"Batareykasi va magnit bolgan istalgan narsa": "Batareykasi va magnit bolgan istalgan narsa",
"(Telifon, sensitive buyumlar, airpods, naushnik, qol soati, tagi yonadigan krasovkalar...)": "(Telifon, sensitive buyumlar, airpods, naushnik, qol soati, tagi yonadigan krasovkalar...)",
"Kukunli buyumlar": "Kukunli buyumlar",
"(Pudra, ten...)": "(Pudra, ten...)",
"Parfumeriya": "Parfumeriya",
"(Barcha Parfumeriya va kosmetika, yuvinish maxsulotlari)": "(Barcha Parfumeriya va kosmetika, yuvinish maxsulotlari)",
"Otkir tigli va sovuq qirollar": "Otkir tigli va sovuq qirollar",
"(Pichoq, qaychi, miltiq...)": "(Pichoq, qaychi, miltiq...)",
"Zargarklik buyumlari": "Zargarklik buyumlari",
"(Tilla, kumush, olmos, braslit...)": "(Tilla, kumush, olmos, braslit...)",
"Oziq ovqat": "Oziq ovqat",
"Dron, avtomat qirollar": "Dron, avtomat qirollar",
"Diniy kitob va diniy buyumlar": "Diniy kitob va diniy buyumlar",
"Agar sizda g'ayrioddiy yoki noaniq mahsulot bo'lsa, albatta buyurtma qilishdan oldin so'rashingiz tavsiya etiladi.": "Agar sizda g'ayrioddiy yoki noaniq mahsulot bo'lsa, albatta buyurtma qilishdan oldin so'rashingiz tavsiya etiladi.",
"Dori darmon va med texnika": "Dori darmon va med texnika",
"Manzilni tekshirish": "Manzilni tekshirish",
"Avia pochta manzili orqali yuborilishi taqiqlangan mahsulot buyurtma qilgan bo'lsangiz, u avtomatik ravishda avtokargo yukiga (avto) o'tkaziladi. Shunday qilib, yukingiz Xitoy omborida qolib ketmaydi.": "Avia pochta manzili orqali yuborilishi taqiqlangan mahsulot buyurtma qilgan bo'lsangiz, u avtomatik ravishda avtokargo yukiga (avto) o'tkaziladi. Shunday qilib, yukingiz Xitoy omborida qolib ketmaydi.",
"Shu bilan birga, Aviada ham, Avtoda ham taqiqlangan mahsulot yuborilgan bo'lsa bunday holatda mahsulot O'zbekistonga yuborilmaydi va bu uchun javobgarlik mijozga yuklanadi.": "Shu bilan birga, ''Aviada'' ham, ''Avtoda'' ham taqiqlangan mahsulot yuborilgan bo'lsa bunday holatda mahsulot O'zbekistonga yuborilmaydi va bu uchun javobgarlik mijozga yuklanadi.",
"Manzil": "Manzil",
"Ish vaqti": "Ish vaqti",
"Telefon": "Telefon",
"Telegram admin": "Telegram admin",
"Telegram kanal": "Telegram kanal",
"Barchasi": "Barchasi",
"Yig'ilmoqda": "Yig'ilmoqda",
"Yo'lda": "Yo'lda",
"Bojxonada": "Bojxonada",
"Toshkent omboriga yetib keldi": "Toshkent omboriga yetib keldi",
"Topshirish punktiga yuborildi": "Topshirish punktiga yuborildi",
"Qabul qilingan": "Qabul qilingan",
"ID orqali izlash": "ID orqali izlash",
"Filter": "Filter",
"Transport": "Transport",
"Reys raqami": "Reys raqami",
"Avto": "Avto",
"Avia": "Avia",
"Mahsulotlar ogirligi": "Mahsulotlar ogirligi",
"Faol": "Faol",
"Kutilmoqda": "Kutilmoqda",
"Faol emas": "Faol emas",
"Xatolik yuz berdi!": "Xatolik yuz berdi!",
"Akkaunt faol emas!": "Akkaunt faol emas!",
"Avto kargoda bir maxsulotdan seriyalab istalgan katta miqdorda olish mumkin. Doimiykop yuk oluvchi ijozlar uchun maxsus arzonlashtrilgan narxlarimiz bor": "Avto kargoda bir maxsulotdan seriyalab istalgan katta miqdorda olish mumkin. Doimiykop yuk oluvchi ijozlar uchun maxsus arzonlashtrilgan narxlarimiz bor",
"Umumiy narxi": "Umumiy narxi",
"Buyurtmalar soni": "Buyurtmalar soni",
"Bekor qilish": "Bekor qilish",
"Haqiqatan ham profildan chiqmoqchimisiz?": "Haqiqatan ham profildan chiqmoqchimisiz?",
"Mahsulotlar": "Mahsulotlar",
"IN_CUSTOMS": "BOJXONADA",
"COLLECTING": "YIG'ILMOQDA",
"ON_THE_WAY": "YO'LDА",
"Ogirligi": "Ogirligi",
"IN_WAREHOUSE": "OMBORDA",
"Trek ID": "Trek ID",
"Hech qanday ma'lumot topilmadi": "Hech qanday ma'lumot topilmadi",
"Narxi": "Narxi",
"Yetkazish tafsilotlari": "Yetkazish tafsilotlari",
"som": "so'm",
"Umumiy narx": "Umumiy narx",
"Yopish": "Yopish",
"Passportlarim": "Passportlarim",
"Hali pasport qo'shilmagan": "Hali pasport qo'shilmagan",
"Yangi pasport qo'shish uchun tugmani bosing": "Yangi pasport qo'shish uchun tugmani bosing",
"Yangi pasport qo'shish": "Yangi pasport qo'shish",
"Passport malumotlarim": "Passport malumotlarim",
"Tez ID": "Tez ID",
"Toliq ismi": "Toliq ismi",
"Passport seriya": "Passport seriya",
"Tugilgan kun": "Tugilgan kun",
"Limit": "Limit",
"Qo'shish": "Qo'shish",
"Passport muvaffaqqiyatli qo'shildi": "Passport muvaffaqqiyatli qo'shildi",
"Yaxshi": "Yaxshi",
"To'langan": "To'langan",
"To'lanmagan": "To'lanmagan",
"Mahsulotlar soni": "Mahsulotlar soni",
"Yetkazish vaqti": "Yetkazish vaqti",
"To'lov usuli": "To'lov usuli",
"To'lash": "To'lash",
"Bank kartasi": "Bank kartasi",
"Naqt pul": "Naqt pul",
"Tolov muvaffaqqiyatli otdi": "Tolov muvaffaqqiyatli otdi",
"Chop etilmoqda": "Chop etilmoqda",
"Toʻlovingiz tasdiqlandi!": "Toʻlovingiz tasdiqlandi!",
"Iltimos ozroq kutib turing!": "Iltimos ozroq kutib turing!",
"Karta nomi": "Karta nomi",
"Karta raqami": "Karta raqami",
"Muddati": "Muddati",
"ilovasi orqali to'lash": "ilovasi orqali to'lash",
"Karta raqami xato": "Karta raqami xato",
"Amal qilish muddati xato": "Amal qilish muddati xato",
"Nusxa olingan": "Nusxa olingan",
"Avto manzili nusxalandi!": "Avto manzili nusxalandi!",
"Avia manzili nusxalandi!": "Avia manzili nusxalandi",
"Rasmni o'zgartirish": "Rasmni o'zgartirish",
"Rasmni o'chirish": "Rasmni o'chirish",
"Bildirishnomalar": "Bildirishnomalar",
"Xitoy omborlari manzili": "Xitoy omborlari manzili",
"Yordam markazi": "Yordam markazi",
"Chiqish": "Chiqish",
"Hisobingizni ochirish": "Hisobingizni ochirish",
"Bizning Xitoy manzilimiz": "Bizning Xitoy manzilimiz",
"Taobao, pinduoduo, 1688 ,alibaba va Xitoyning istalgan platformasiga kiritish uchun": "Taobao, pinduoduo, 1688 ,alibaba va Xitoyning istalgan platformasiga kiritish uchun",
"Xitoy omborlarimiz manzilini programmaga kiriting": "Xitoy omborlarimiz manzilini programmaga kiriting",
"Diqqat! Iltimos, Xitoy omborimiz manzilini Xitoy programmalariga kiritganingizdan so'ng, kiritilgan holatdagi skrenshotni bizga yuborib, tekshirtiring": "Diqqat! Iltimos, Xitoy omborimiz manzilini Xitoy programmalariga kiritganingizdan so'ng, kiritilgan holatdagi skrenshotni bizga yuborib, tekshirtiring",
"Xitoy ombori manzilini to'g'ri kiritish, mahsulotingiz yo'qolib qolish oldini oladi.": "Xitoy ombori manzilini to'g'ri kiritish, mahsulotingiz yo'qolib qolish oldini oladi.",
"Agar sizda savol tug'ilsa yoki biron narsaga tushunmasangiz bizga murojaat qiling": "Agar sizda savol tug'ilsa yoki biron narsaga tushunmasangiz bizga murojaat qiling",
"Skrenshot rasmini yuklang": "Skrenshot rasmini yuklang",
"Rasmni shu yerga yuklang": "Rasmni shu yerga yuklang",
"Yordam xizmati": "Yordam xizmati",
"Xabar yozing...": "Xabar yozing...",
"Yuborish": "Yuborish",
"Passport qo'shishda xatolik yuz berdi": "Passport qo'shishda xatolik yuz berdi",
"Xatolik yuz berdi": "Xatolik yuz berdi",
"Kodsiz tovarlar": "Kodsiz tovarlar"
}

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

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

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

View 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 yoqmi?')}
</Text>
<TouchableOpacity
style={Loginstyle.dropdownItem}
onPress={() => navigation.navigate('Register')}
>
<Text
style={{
color: '#28A7E8',
fontSize: 14,
fontWeight: '500',
}}
>
{t('Royxatdan otish')}
</Text>
</TouchableOpacity>
</View>
</View>
</View>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
</ImageBackground>
);
};
export default Login;

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

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

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

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

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

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

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

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

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

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

View 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('Royxatdan otganmisz')}
</Text>
<View style={styles.btnContainer}>
<View style={{ gap: 4 }}>
<Text style={styles.helperText}>
{t("Botdan ro'yxatdan otganmisiz")}
</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 royxatdan o'tmoqchimisiz")}
</Text>
<TouchableOpacity
onPress={() => navigation.navigate('Register')}
style={styles.button}
>
<Text style={styles.btnText}>{t('Royxatdan otish')}</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,
},
});

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

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

View File

@@ -0,0 +1,140 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import React, { useEffect, useState } from 'react';
import { Alert, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import ReactNativeBiometrics from 'react-native-biometrics';
import GesturePassword from 'react-native-gesture-password';
const LOCK_ENABLED = 'LOCK_ENABLED';
const LOCK_TYPE = 'LOCK_TYPE';
const LOCK_PASSWORD = 'LOCK_PASSWORD';
const LOCK_PATTERN = 'LOCK_PATTERN';
const LOCK_BIOMETRIC = 'LOCK_BIOMETRIC';
type Props = { onAuthenticated: () => void };
const LockScreen = ({ onAuthenticated }: Props) => {
const [enabled, setEnabled] = useState(false);
const [lockType, setLockType] = useState<'pin' | 'password' | 'pattern'>(
'pin',
);
const [password, setPassword] = useState('');
const [pattern, setPattern] = useState('');
const [biometricAvailable, setBiometricAvailable] = useState(false);
useEffect(() => {
const load = async () => {
const lockEnabled = await AsyncStorage.getItem(LOCK_ENABLED);
if (lockEnabled === 'true') {
const type = await AsyncStorage.getItem(LOCK_TYPE);
const pass = await AsyncStorage.getItem(LOCK_PASSWORD);
const pat = await AsyncStorage.getItem(LOCK_PATTERN);
setLockType(type as any);
setPassword(pass || '');
setPattern(pat || '');
setEnabled(true);
const bioEnabled = await AsyncStorage.getItem(LOCK_BIOMETRIC);
if (bioEnabled === 'true') {
const rnBiometrics = new ReactNativeBiometrics();
const { available } = await rnBiometrics.isSensorAvailable();
if (available) {
setBiometricAvailable(true);
try {
const { success } = await rnBiometrics.simplePrompt({
promptMessage: 'Unlock with biometrics',
});
if (success) onAuthenticated();
} catch (e) {}
}
}
}
};
load();
}, [onAuthenticated]);
if (!enabled) return null;
const handlePatternEnd = async (inputPattern: string) => {
if (!pattern) {
// pattern birinchi marta saqlanmoqda
await AsyncStorage.setItem(LOCK_PATTERN, inputPattern);
setPattern(inputPattern);
Alert.alert('OK', 'Pattern saqlandi');
} else {
if (inputPattern === pattern) {
onAuthenticated();
} else {
Alert.alert('Xato', 'Pattern notogri');
}
}
};
const handleUnlockPasswordOrPin = (input: string) => {
if (input === password) {
onAuthenticated();
} else {
Alert.alert('Xato', 'Parol notogri');
}
};
return (
<View style={styles.container}>
<Text style={styles.title}>Unlock</Text>
{lockType === 'pattern' ? (
<GesturePassword
style={{ flex: 1 }}
onEnd={handlePatternEnd}
interval={300}
/>
) : (
<View style={{ marginTop: 50 }}>
<TouchableOpacity
style={styles.button}
onPress={() => handleUnlockPasswordOrPin(password)}
>
<Text style={styles.buttonText}>
Unlock with {lockType.toUpperCase()}
</Text>
</TouchableOpacity>
</View>
)}
{biometricAvailable && (
<TouchableOpacity
style={[styles.button, { marginTop: 10, backgroundColor: '#4CAF50' }]}
onPress={async () => {
const rnBiometrics = new ReactNativeBiometrics();
try {
const { success } = await rnBiometrics.simplePrompt({
promptMessage: 'Unlock with biometrics',
});
if (success) onAuthenticated();
} catch (e) {}
}}
>
<Text style={styles.buttonText}>Unlock with Biometrics</Text>
</TouchableOpacity>
)}
</View>
);
};
export default LockScreen;
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
padding: 20,
backgroundColor: '#fff',
},
title: { fontSize: 24, textAlign: 'center', marginBottom: 20 },
button: {
backgroundColor: '#28A7E8',
padding: 15,
borderRadius: 8,
alignItems: 'center',
},
buttonText: { color: '#fff', fontSize: 18, fontWeight: 'bold' },
});

View File

@@ -0,0 +1,96 @@
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useQuery } from '@tanstack/react-query';
import { branchApi } from 'api/branch';
import NavbarBack from 'components/NavbarBack';
import NoResult from 'components/NoResult';
import * as 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 ArrowRightUnderline from 'svg/ArrowRightUnderline';
interface BranchesProps {}
const Branches = (props: BranchesProps) => {
const navigation = useNavigation<NativeStackNavigationProp<any>>();
const { t } = useTranslation();
const { data, isError } = useQuery({
queryKey: ['branchList'],
queryFn: branchApi.branchList,
});
if (isError || data?.length === 0) {
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={{ flex: 1 }}>
<NavbarBack title={t('Filiallar royxati')} />
<NoResult message="Xatolik yuz berdi" />
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={{ flex: 1 }}>
<NavbarBack title={t('Filiallar royxati')} />
<View style={styles.scrollWrapper}>
<ScrollView contentContainerStyle={styles.scrollContainer}>
{data &&
data.map(e => (
<TouchableOpacity
key={e.id}
style={styles.card}
onPress={() =>
navigation.navigate('ListBranches', { branchId: e.id })
}
>
<View>
<Text style={styles.title}>{e.name}</Text>
<Text style={styles.subtitle}>{e.address}</Text>
</View>
<ArrowRightUnderline color="#28A7E8" />
</TouchableOpacity>
))}
</ScrollView>
</View>
</SafeAreaView>
);
};
export default Branches;
const styles = StyleSheet.create({
scrollWrapper: {
flex: 1,
},
scrollContainer: {
padding: 8,
},
card: {
backgroundColor: '#FFFFFF',
borderRadius: 8,
padding: 16,
marginBottom: 12,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
title: {
fontSize: 18,
fontWeight: '600',
color: '#000',
marginBottom: 6,
},
subtitle: {
fontSize: 16,
fontWeight: '500',
color: '#000000B2',
},
});

View File

@@ -0,0 +1,243 @@
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useQuery } from '@tanstack/react-query';
import { Branch, branchApi } from 'api/branch';
import BottomModal from 'components/BottomModal';
import LoadingScreen from 'components/LoadingScreen';
import NavbarBack from 'components/NavbarBack';
import Navigation from 'components/Navigation';
import React, { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import WebView from 'react-native-webview';
import Minus from 'svg/Minus';
import Plus from 'svg/Plus';
const ListBranches = () => {
const route = useRoute<RouteProp<any>>();
const webviewRef = useRef<WebView>(null);
const [webViewReady, setWebViewReady] = useState(false);
const [selectedBranch, setSelectedBranch] = useState<Branch | null>(null);
const [isModalVisible, setModalVisible] = useState(false);
const navigation = useNavigation<NativeStackNavigationProp<any>>();
const { t } = useTranslation();
const { data } = useQuery({
queryKey: ['branchList'],
queryFn: branchApi.branchList,
});
useEffect(() => {
if (webViewReady && route.params?.branchId) {
const branch = data && data.find(b => b.id === route?.params?.branchId);
if (branch) {
setSelectedBranch(branch);
setModalVisible(true);
const jsCode = `
map.setCenter([${branch.latitude}, ${branch.longitude}], 14);
placemark${branch.id}?.balloon.open();
true;
`;
webviewRef.current?.injectJavaScript(jsCode);
}
}
}, [webViewReady, route.params]);
const generatePlacemarks = () => {
if (!data || !data.length) return '';
return data
.map(
branch => `
var placemark${branch.id} = new ymaps.Placemark([${branch.latitude}, ${branch.longitude}], {
balloonContent: '${branch.name}'
}, {
iconLayout: 'default#image',
iconImageSize: [30, 30],
iconImageOffset: [-15, -30]
});
placemark${branch.id}.events.add('click', function () {
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'branch_click',
id: ${branch.id}
}));
});
map.geoObjects.add(placemark${branch.id});
`,
)
.join('\n');
};
const html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://api-maps.yandex.ru/2.1/?lang=ru_RU"></script>
<style>
html, body, #map {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<div id="map"></div>
<script>
let map;
ymaps.ready(function () {
map = new ymaps.Map("map", {
center: [40.5, 67.9],
zoom: 6,
controls: []
});
${generatePlacemarks()}
window.ReactNativeWebView.postMessage("map_ready");
});
function zoomIn() {
if (map) map.setZoom(map.getZoom() + 1);
}
function zoomOut() {
if (map) map.setZoom(map.getZoom() - 1);
}
</script>
</body>
</html>
`;
const handleZoom = (type: 'in' | 'out') => {
const command = type === 'in' ? 'zoomIn(); true;' : 'zoomOut(); true;';
webviewRef.current?.injectJavaScript(command);
};
if (!data) return <LoadingScreen />;
return (
<SafeAreaView style={styles.container}>
<NavbarBack title={t('Filiallar royxati')} />
{!webViewReady && (
<View style={{ width: '100%', height: '100%', margin: 'auto' }}>
<LoadingScreen />
</View>
)}
<View style={styles.container}>
<WebView
ref={webviewRef}
originWhitelist={['*']}
source={{ html }}
javaScriptEnabled
domStorageEnabled
onMessage={event => {
try {
const message = event.nativeEvent.data;
if (message === 'map_ready') {
setWebViewReady(true);
return;
}
const parsed = JSON.parse(message);
if (parsed.type === 'branch_click') {
const branchItem =
data && data.find((b: Branch) => b.id === parsed.id);
if (branchItem) {
setSelectedBranch(branchItem);
setModalVisible(true);
}
}
} catch (e) {
console.warn('WebView message parse error:', e);
}
}}
style={{ flex: 1 }}
/>
{webViewReady && (
<View style={{ position: 'absolute', bottom: 0 }}>
<View style={styles.zoomControls}>
<TouchableOpacity
style={styles.zoomButton}
onPress={() => handleZoom('in')}
>
<Text>
<Plus color="#DEDEDE" />
</Text>
</TouchableOpacity>
<View style={styles.divider} />
<TouchableOpacity
style={styles.zoomButton}
onPress={() => handleZoom('out')}
>
<Text>
<Minus color="#DEDEDE" />
</Text>
</TouchableOpacity>
</View>
<TouchableOpacity
style={styles.button}
onPress={() => navigation.navigate('Branches')}
>
<Text style={styles.buttonText}>{t('Manzilni tekshirish')}</Text>
</TouchableOpacity>
</View>
)}
<BottomModal
visible={isModalVisible}
onClose={() => setModalVisible(false)}
branch={selectedBranch}
/>
</View>
<Navigation />
</SafeAreaView>
);
};
export default ListBranches;
const styles = StyleSheet.create({
container: { flex: 1 },
zoomControls: {
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#373737',
borderRadius: 20,
width: '10%',
padding: 4,
marginLeft: '85%',
bottom: 20,
},
zoomButton: {
borderRadius: 8,
width: 40,
height: 40,
marginVertical: 4,
justifyContent: 'center',
alignItems: 'center',
},
divider: { height: 1, width: '100%', backgroundColor: '#DEDEDE' },
button: {
bottom: 10,
width: '95%',
margin: 'auto',
alignSelf: 'center',
backgroundColor: '#009CFF',
paddingVertical: 14,
paddingHorizontal: 28,
borderRadius: 10,
elevation: 4,
},
buttonText: {
color: '#fff',
fontWeight: '600',
fontSize: 16,
textAlign: 'center',
},
});

View File

@@ -0,0 +1,272 @@
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import NavbarBack from 'components/NavbarBack';
import Navigation from 'components/Navigation';
import * as 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 Tabs from '../../home/ui/Tabs';
interface CargoPricesProps {}
const CargoPrices = (props: CargoPricesProps) => {
const { t } = useTranslation();
const [activeTab, setActiveTab] = React.useState<'avia' | 'auto'>('avia');
const navigation = useNavigation<NativeStackNavigationProp<any>>();
return (
<SafeAreaView style={{ flex: 1 }}>
<NavbarBack title={t('Kargo narxlari')} />
<ScrollView style={{ flex: 1 }}>
<View style={styles.container}>
<Tabs activeTab={activeTab} setActiveTab={setActiveTab} />
{activeTab === 'avia' && (
<View style={{ marginTop: 20, gap: 10, marginBottom: 20 }}>
<View style={styles.cardWhite}>
<View style={styles.priceCard}>
<Text style={styles.titleBlack}>
{t('Oddiy maxsulotlar')}
</Text>
<Text style={[styles.titleBlack, { fontSize: 16 }]}>
9.2$ /1kg
</Text>
</View>
<Text style={styles.desc}>
{t('(Katta miqdordagi yuklar kelishuv asosida)')}
</Text>
</View>
<View style={styles.cardWhite}>
<View style={styles.priceCard}>
<Text style={styles.titleBlack}>{t('Brend buyumlar')}</Text>
<Text style={[styles.titleBlack, { fontSize: 16 }]}>
12.2$ /1kg
</Text>
</View>
<Text style={styles.desc}>
{t('(Karobka,dokumentlar bilan birga)')}
</Text>
</View>
<View style={styles.cardWhite}>
<View style={styles.priceCard}>
<Text style={styles.titleBlack}>
{t(
'Avia pochtada bir maxsulotdan seriyalab olish mumkin emas',
)}
</Text>
</View>
<Text style={styles.desc}>
{t(
'(Bu kargo narxini osishiga olib keladi. Seriyali buyularni avto kargo orqali olib kelish arzonga tushadi)',
)}
</Text>
</View>
<View style={[styles.cardWhite, { backgroundColor: '#DFF2FD' }]}>
<View style={styles.priceCard}>
<Text style={[styles.titleBlack, { color: '#28A7E8' }]}>
{t('Yetib kelish vaqti')}
</Text>
<Text
style={[
styles.titleBlack,
{ fontSize: 16, color: '#28A7E8' },
]}
>
7-10 {t('kun')}
</Text>
</View>
<Text style={[styles.desc, { color: '#28A7E8B2' }]}>
{t(
'Yuklarni yetib elish vaqti oxirgi qadoqlash kunidan boshlab xisoblanadi',
)}
</Text>
</View>
<View style={styles.cardWhite}>
<View style={styles.priceCard}>
<Text style={styles.titleBlack}>
{t('Minimal miqdor talab qilinmaydi.')}
</Text>
</View>
<Text style={styles.desc}>
{t(
"ya'ni, qancha gramm mahsulot olsangiz, shuncha og'irligi (gramm) uchun to'lov qilasiz.",
)}
</Text>
</View>
<View style={[styles.card]}>
<Text style={[styles.titleBlack, { color: '#FF6363' }]}>
{t('Muhim!')}
</Text>
</View>
<View style={[styles.cardWhite, { backgroundColor: '#DFF2FD' }]}>
<View style={styles.priceCard}>
<Text style={[styles.titleBlack, { color: '#28A7E8' }]}>
{t('Filiallargacha yetkazib berish - bepul.')}
</Text>
</View>
<Text style={[styles.desc, { color: '#000000' }]}>
{t('Batafsil')}: @CPcargo_admin
</Text>
</View>
<View style={[styles.card]}>
<Text style={[styles.titleBlack, { color: '#000000B2' }]}>
{t(
"Agar siz yashayotgan hududda bizning filialimiz mavjud bo'lmasa, o'zingizga eng yaqin bo'lgan filialni tanlab, ro'yhatdan o'tishingiz mumkin.",
)}
</Text>
</View>
<TouchableOpacity
style={[styles.card]}
onPress={() => navigation.navigate('ListBranches')}
>
<Text style={[styles.titleBlack, { color: '#28A7E8' }]}>
{t("Filiallarimiz ro'yhati ilovada mavjud")}
</Text>
</TouchableOpacity>
</View>
)}
{activeTab === 'auto' && (
<View style={{ marginTop: 20, gap: 10, marginBottom: 20 }}>
<View style={styles.cardWhite}>
<View style={styles.priceCard}>
<Text style={styles.titleBlack}>0-30 kg</Text>
<Text style={[styles.titleBlack, { fontSize: 16 }]}>
7$ /1kg
</Text>
</View>
</View>
<View style={styles.cardWhite}>
<View style={styles.priceCard}>
<Text style={styles.titleBlack}>30-100kg</Text>
<Text style={[styles.titleBlack, { fontSize: 16 }]}>
6.5$ /1kg
</Text>
</View>
</View>
<View style={styles.cardWhite}>
<View style={styles.priceCard}>
<Text style={styles.titleBlack}>
100kg {t('dan boshlab')}
</Text>
<Text style={[styles.titleBlack, { fontSize: 16 }]}>
6$ /1kg
</Text>
</View>
</View>
<View style={styles.cardWhite}>
<Text style={styles.desc}>
{t(
'Avto kargoda bir maxsulotdan seriyalab istalgan katta miqdorda olish mumkin. Doimiykop yuk oluvchi ijozlar uchun maxsus arzonlashtrilgan narxlarimiz bor',
)}
</Text>
</View>
<View style={[styles.cardWhite, { backgroundColor: '#DFF2FD' }]}>
<View style={styles.priceCard}>
<Text style={[styles.titleBlack, { color: '#28A7E8' }]}>
{t('Yetib kelish vaqti')}
</Text>
<Text
style={[
styles.titleBlack,
{ fontSize: 16, color: '#28A7E8' },
]}
>
10-20 {t('kun')}
</Text>
</View>
<Text style={[styles.desc, { color: '#28A7E8B2' }]}>
{t(
'Yuklarni yetib elish vaqti oxirgi qadoqlash kunidan boshlab xisoblanadi',
)}
</Text>
</View>
<View style={styles.cardWhite}>
<View style={styles.priceCard}>
<Text style={styles.titleBlack}>
{t('Minimal miqdor talab qilinmaydi.')}
</Text>
</View>
<Text style={styles.desc}>
{t(
"ya'ni, qancha gramm mahsulot olsangiz, shuncha og'irligi (gramm) uchun to'lov qilasiz.",
)}
</Text>
</View>
<View style={[styles.card]}>
<Text style={[styles.titleBlack, { color: '#FF6363' }]}>
{t('Muhim!')}
</Text>
</View>
<View style={[styles.cardWhite, { backgroundColor: '#DFF2FD' }]}>
<View style={styles.priceCard}>
<Text style={[styles.titleBlack, { color: '#28A7E8' }]}>
{t('Filiallargacha yetkazib berish - bepul.')}
</Text>
</View>
<Text style={[styles.desc, { color: '#000000' }]}>
{t('Batafsil')}: @CPcargo_admin
</Text>
</View>
<View style={[styles.card]}>
<Text style={[styles.titleBlack, { color: '#000000B2' }]}>
{t(
"Agar siz yashayotgan hududda bizning filialimiz mavjud bo'lmasa, o'zingizga eng yaqin bo'lgan filialni tanlab, ro'yhatdan o'tishingiz mumkin.",
)}
</Text>
</View>
<TouchableOpacity
style={[styles.card]}
onPress={() => navigation.navigate('ListBranches')}
>
<Text style={[styles.titleBlack, { color: '#28A7E8' }]}>
{t("Filiallarimiz ro'yhati ilovada mavjud")}
</Text>
</TouchableOpacity>
</View>
)}
</View>
</ScrollView>
<Navigation />
</SafeAreaView>
);
};
export default CargoPrices;
const styles = StyleSheet.create({
container: {
marginTop: 20,
},
card: {
width: '95%',
gap: 5,
margin: 'auto',
},
cardWhite: {
backgroundColor: '#FFFFFF',
width: '95%',
gap: 5,
margin: 'auto',
padding: 10,
borderRadius: 8,
},
titleBlack: {
fontSize: 16,
fontWeight: '500',
},
desc: {
color: '#000000B2',
fontSize: 14,
fontWeight: '400',
},
priceCard: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
});

View File

@@ -0,0 +1,95 @@
export type Cargo = 'auto' | 'avia';
export interface CargoItem {
party: string;
cargo: Cargo;
start: string[] | string;
end: string;
isAni: boolean;
}
export const fakeDataList: CargoItem[] = [
{
party: 'CP21',
cargo: 'auto',
start: [
'2025-07-26', // Shanba
'2025-07-27', // Yakshanba
'2025-07-28', // Dushanba
'2025-07-29', // Seshanba
'2025-07-30', // Chorshanba
'2025-07-31', // Payshanba
'2025-08-01', // Juma
],
end: '2025-07-25',
isAni: true,
},
{
party: 'CP21',
cargo: 'auto',
start: [
'2025-07-26', // Shanba
'2025-07-27', // Yakshanba
'2025-07-28', // Dushanba
'2025-07-29', // Seshanba
'2025-07-30', // Chorshanba
'2025-07-31', // Payshanba
'2025-08-01', // Juma
],
end: '2025-07-25',
isAni: true,
},
{
party: 'CP22',
cargo: 'auto',
start: '16.06.2025',
isAni: false,
end: '22.06.2025',
},
{
party: 'CP20',
cargo: 'avia',
end: '2025-08-04',
start: [
'2025-07-26', // Shanba
'2025-07-27', // Yakshanba
'2025-07-28', // Dushanba
'2025-07-29', // Seshanba
'2025-07-30', // Chorshanba
'2025-07-31', // Payshanba
'2025-08-01', // Juma
],
isAni: true,
},
];
interface Branch {
id: number;
name: string;
latitude: number;
longitude: number;
address: string;
}
export const fakeBranches: Branch[] = [
{
id: 1,
name: 'Toshkent Filiali',
latitude: 41.311081,
longitude: 69.240562,
address: 'Amir Temur kochasi, 12-uy',
},
{
id: 2,
name: 'Fargona Filiali',
latitude: 40.384213,
longitude: 71.784287,
address: 'Mustaqillik kochasi, 45-uy',
},
{
id: 3,
name: 'Samarqand Filiali',
latitude: 39.654166,
longitude: 66.959722,
address: 'Registon kochasi, 10-uy',
},
];

View File

@@ -0,0 +1,104 @@
'use client';
import { useQuery } from '@tanstack/react-query';
import calendarAPi from 'api/calendar';
import LoadingScreen from 'components/LoadingScreen';
import Navbar from 'components/Navbar';
import Navigation from 'components/Navigation';
import { useCallback, useMemo, useState } from 'react';
import {
RefreshControl,
ScrollView,
useWindowDimensions,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import Pages from './Pages';
import PartyCarousel from './PartyCarousel';
import { HomeStyle } from './styled';
import Tabs from './Tabs';
import TabsAuto from './TabsAuto';
import TabsAvia from './TabsAvia';
const Home = () => {
const [activeTab, setActiveTab] = useState<'avia' | 'auto'>('avia');
const [refreshing, setRefreshing] = useState(false);
const { width: screenWidth } = useWindowDimensions();
const scale = screenWidth < 360 ? 0.85 : 1;
const styles = useMemo(() => HomeStyle(scale), [scale]);
const {
data: autoData,
isLoading: autoLoad,
refetch: refetchAuto,
isFetching: fetchAuto,
} = useQuery({
queryKey: ['calendar', 'AUTO'],
queryFn: () => calendarAPi.getCalendar({ cargoType: 'AUTO' }),
});
const {
data: aviaData,
isLoading: aviaLoad,
refetch: refetchAvia,
isFetching: fetchAvia,
} = useQuery({
queryKey: ['calendarAvia', 'AVIA'],
queryFn: () => calendarAPi.getCalendar({ cargoType: 'AVIA' }),
});
const onRefresh = useCallback(async () => {
setRefreshing(true);
try {
await Promise.all([refetchAuto(), refetchAvia()]);
} finally {
setRefreshing(false);
}
}, [refetchAuto, refetchAvia]);
const refreshControl = useMemo(
() => <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />,
[refreshing, onRefresh],
);
const activeTabContent = useMemo(() => {
if (activeTab === 'auto') {
return <TabsAuto />;
} else if (activeTab === 'avia') {
return <TabsAvia />;
}
return null;
}, [activeTab]);
if (autoLoad || aviaLoad || fetchAuto || fetchAvia) {
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={styles.container}>
<Navbar />
<LoadingScreen />
<Navigation />
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={styles.container}>
<Navbar />
<ScrollView
refreshControl={refreshControl}
removeClippedSubviews={true}
keyboardShouldPersistTaps="handled"
>
<PartyCarousel autoData={autoData} aviaData={aviaData} />
<Tabs setActiveTab={setActiveTab} activeTab={activeTab} />
{activeTabContent}
<Pages />
</ScrollView>
<Navigation />
</View>
</SafeAreaView>
);
};
export default Home;

View File

@@ -0,0 +1,142 @@
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
StyleSheet,
Text,
TouchableOpacity,
View,
useWindowDimensions,
} from 'react-native';
import ArrowRightUnderline from 'svg/ArrowRightUnderline';
import Usd from 'svg/Dollar';
import InfoIcon from 'svg/Info';
import Store from 'svg/Store';
const Pages = () => {
const { width: screenWidth } = useWindowDimensions();
const navigation = useNavigation<NativeStackNavigationProp<any>>();
const scale = screenWidth < 360 ? 0.85 : 1;
const { t } = useTranslation();
const styles = makeStyles(scale);
return (
<View style={styles.container}>
<TouchableOpacity
onPress={() => navigation.navigate('CargoPrices')}
style={styles.card}
>
<View style={styles.text}>
<Usd
color="#28A7E8"
width={28 * scale}
height={28 * scale}
colorCircle="#28A7E8"
/>
<Text style={styles.title}>{t('Kargo narxlari')}</Text>
</View>
<ArrowRightUnderline
color="#000000"
width={24 * scale}
height={24 * scale}
/>
</TouchableOpacity>
<TouchableOpacity
onPress={() => navigation.navigate('RestrictedProduct')}
style={styles.card}
>
<View style={styles.text}>
<InfoIcon color="#28A7E8" width={28 * scale} height={28 * scale} />
<Text style={styles.title}>{t('Taqiqlangan buyumlar')}</Text>
</View>
<ArrowRightUnderline
color="#000000"
width={24 * scale}
height={24 * scale}
/>
</TouchableOpacity>
{/* <View style={styles.card}>
<View style={styles.text}>
<Auto
color="#28A7E8"
width={28 * scale}
height={28 * scale}
view="-4"
/>
<Text style={styles.title}>Shaxar boylab yetkazish</Text>
</View>
<ArrowRightUnderline
color="#000000"
width={24 * scale}
height={24 * scale}
/>
</View> */}
<TouchableOpacity
style={styles.card}
onPress={() => navigation.navigate('ListBranches')}
>
<View style={styles.text}>
<Store color="#28A7E8" width={28 * scale} height={28 * scale} />
<Text style={styles.title}>{t('Filiallar royxati')}</Text>
</View>
<ArrowRightUnderline
color="#000000"
width={24 * scale}
height={24 * scale}
/>
</TouchableOpacity>
{/* <TouchableOpacity
style={styles.card}
onPress={() => navigation.navigate('Uncodified')}
>
<View style={styles.text}>
<BarCode color="#28A7E8" width={28 * scale} height={28 * scale} />
<Text style={styles.title}>{t('Kodsiz tovarlar')}</Text>
</View>
<ArrowRightUnderline
color="#000000"
width={24 * scale}
height={24 * scale}
/>
</TouchableOpacity> */}
</View>
);
};
const makeStyles = (scale: number) =>
StyleSheet.create({
container: {
width: '100%',
marginLeft: 'auto',
marginRight: 'auto',
marginTop: 10 * scale,
borderRadius: 12 * scale,
padding: 12 * scale,
gap: 10 * scale,
},
card: {
flexDirection: 'row',
justifyContent: 'space-between',
backgroundColor: '#FFFFFF',
shadowColor: '#F2FAFF',
padding: 15 * scale,
borderRadius: 8 * scale,
alignItems: 'center',
},
text: {
flexDirection: 'row',
alignItems: 'center',
gap: 10 * scale,
},
title: {
fontSize: 16 * scale,
fontWeight: '600',
},
});
export default Pages;

View File

@@ -0,0 +1,276 @@
import AnimatedIcon from 'components/AnimatedIcon';
import React, { useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
FlatList,
Modal,
Pressable,
StyleSheet,
Text,
useWindowDimensions,
View,
} from 'react-native';
import Auto from 'svg/Auto';
import Avia from 'svg/Avia';
import BoxCreate from 'svg/BoxCreate';
import BoxSuccess from 'svg/BoxSuccess';
import { HomeStyle } from './styled';
const PartyCarousel = ({
aviaData,
autoData,
}: {
autoData: any;
aviaData: any;
}) => {
const { width: screenWidth } = useWindowDimensions();
const scale = screenWidth < 360 ? 0.85 : 1;
const cardWidth = screenWidth * 0.95;
const styles = useMemo(() => HomeStyle(scale), [scale]);
const { t } = useTranslation();
const statusConfig: any = {
COLLECTING: {
backgroundColor: '#28A7E8',
textColor: '#fff',
icon: <BoxCreate color="#fff" width={18} height={18} />,
},
ON_THE_WAY: {
backgroundColor: '#5eda62ff',
textColor: '#fff',
icon: <Auto color="#fff" width={18} height={18} />,
},
IN_WAREHOUSE: {
backgroundColor: '#28A7E8',
textColor: '#fff',
icon: <BoxSuccess color="#fff" width={18} height={18} />,
},
IN_CUSTOMS: {
backgroundColor: '#28A7E8',
textColor: '#fff',
icon: <Avia color="#fff" width={18} height={18} />,
},
DEFAULT: {
backgroundColor: '#f0f0f0',
textColor: '#555',
icon: <BoxCreate color="#28A7E8" width={18} height={18} />,
},
};
const calendarList = useMemo(() => {
const data: any[] = [];
const prepareList = (calendar: any, type: 'auto' | 'avia') => {
if (!calendar) return;
const weekdays = [
{ key: 'sunday', label: 'Ya' },
{ key: 'monday', label: 'Du' },
{ key: 'tuesday', label: 'Se' },
{ key: 'wednesday', label: 'Cho' },
{ key: 'thursday', label: 'Pa' },
{ key: 'friday', label: 'Ju' },
{ key: 'saturday', label: 'Sha' },
];
data.push({
cargo: type,
party: calendar.cargoType,
isAni: true,
start: weekdays.map((day, index) => {
const start = new Date(calendar.weekStartDate);
start.setDate(start.getDate() + index);
const status =
calendar[day.key as keyof typeof calendar] || 'DEFAULT';
return {
date: start.toISOString().split('T')[0],
weekday: day.label,
status,
};
}),
});
};
prepareList(autoData, 'auto');
prepareList(aviaData, 'avia');
return data;
}, [autoData, aviaData]);
const flatListRef = useRef<FlatList>(null);
const today = useMemo(() => {
const d = new Date();
d.setHours(0, 0, 0, 0);
return d;
}, []);
// --- MODAL STATE ---
const [selectedItem, setSelectedItem] = useState<any>(null);
const [modalVisible, setModalVisible] = useState(false);
const renderItem = ({ item, index }: { item: any; index: number }) => {
const isLast = index === calendarList.length - 1;
return (
<Pressable
onPress={() => {
setSelectedItem(item);
setModalVisible(true);
}}
style={[
styles.autoContainer,
{ width: cardWidth, marginRight: isLast ? 0 : 10 },
]}
>
{item.isAni && (
<>
<View style={styles.cardBody}>
{Array.isArray(item.start) &&
item.start.map((day: any, idx: number) => {
const dateObj = new Date(day.date);
const isToday =
dateObj.toDateString() === today.toDateString();
const config =
statusConfig[day.status] || statusConfig.DEFAULT;
let backgroundColor = config.backgroundColor;
let textColor = config.textColor;
if (isToday) {
backgroundColor = '#22b1f88f';
textColor = '#fff';
}
if (day.weekday === 'Ju') {
backgroundColor = '#4CAF50';
textColor = '#fff';
}
return (
<View
key={idx}
style={[
styles.date,
{
backgroundColor,
alignItems: 'center',
justifyContent: 'center',
height: 70,
},
]}
>
<Text style={[styles.dateLabel, { color: textColor }]}>
{day.date.slice(-2)}
</Text>
<Text style={[styles.dateLabel, { color: textColor }]}>
{t(day.weekday)}
</Text>
</View>
);
})}
</View>
<View style={styles.divider} />
<View style={styles.autoCard}>
<View style={styles.row}>
<View style={styles.rowFull}>
<Text style={styles.reysTitle}>
{item.cargo.toUpperCase()}
</Text>
<View style={styles.animatedIconWrapper}>
<AnimatedIcon type={item.cargo} />
</View>
</View>
</View>
</View>
</>
)}
</Pressable>
);
};
return (
<>
<FlatList
ref={flatListRef}
data={calendarList}
keyExtractor={(_, index) => index.toString()}
renderItem={renderItem}
horizontal
snapToInterval={cardWidth + 10}
decelerationRate="fast"
contentContainerStyle={{
paddingHorizontal: (screenWidth - cardWidth) / 2,
}}
pagingEnabled
style={{ marginBottom: 20 }}
showsHorizontalScrollIndicator={false}
getItemLayout={(_, index) => ({
length: cardWidth,
offset: cardWidth * index,
index,
})}
/>
{/* MODAL */}
<Modal
transparent
visible={modalVisible}
animationType="fade"
onRequestClose={() => setModalVisible(false)}
>
<View style={modalStyles.modalOverlay}>
<View style={modalStyles.modalContent}>
<Text style={modalStyles.modalTitle}>
{t('Yetkazish tafsilotlari')}
</Text>
{selectedItem && (
<>
{selectedItem.start.map((day: any, idx: number) => (
<Text key={idx} style={{ textAlign: 'left', width: '100%' }}>
{day.date} - {t(day.weekday)} - {t(day.status)}
</Text>
))}
</>
)}
<Pressable
onPress={() => setModalVisible(false)}
style={modalStyles.closeButton}
>
<Text style={{ color: '#fff' }}>{t('Yopish')}</Text>
</Pressable>
</View>
</View>
</Modal>
</>
);
};
export default PartyCarousel;
const modalStyles = StyleSheet.create({
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.5)',
justifyContent: 'center',
alignItems: 'center',
},
modalContent: {
width: '80%',
backgroundColor: '#fff',
padding: 20,
borderRadius: 12,
alignItems: 'center',
},
modalTitle: {
fontSize: 18,
fontWeight: '600',
marginBottom: 12,
},
closeButton: {
marginTop: 20,
backgroundColor: '#28A7E8',
paddingVertical: 8,
paddingHorizontal: 20,
borderRadius: 8,
},
});

View File

@@ -0,0 +1,91 @@
import { Dispatch, SetStateAction, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import {
Image,
Text,
TouchableOpacity,
useWindowDimensions,
View,
} from 'react-native';
import LinearGradient from 'react-native-linear-gradient';
import AviaLogo from 'screens/../../assets/bootsplash/Avia.png';
import AutoLogo from 'screens/../../assets/bootsplash/auto.png';
import { HomeStyle } from './styled';
interface Props {
activeTab: string;
setActiveTab: Dispatch<SetStateAction<'avia' | 'auto'>>;
}
const Tabs = ({ activeTab, setActiveTab }: Props) => {
const { width: screenWidth } = useWindowDimensions();
const { t } = useTranslation();
const scale = useMemo(() => (screenWidth < 360 ? 0.85 : 1), [screenWidth]);
const styles = useMemo(() => HomeStyle(scale), [scale]);
const handleTabPress = useCallback(
(type: 'avia' | 'auto') => {
setActiveTab(type);
},
[setActiveTab],
);
const gradientStyle = useMemo(
() => ({
width: '100%' as const,
borderRadius: 8,
flexDirection: 'row' as const,
alignItems: 'center' as const,
paddingHorizontal: 12,
height: 80,
gap: 8,
}),
[],
);
const renderTabButton = useCallback(
(type: 'avia' | 'auto', label: string, logo: any) => {
const isActive = activeTab === type;
const gradientColors = isActive
? ['#28A7E8', '#28A7E8']
: ['#28a8e82d', '#28A7E8'];
const textStyle = [
styles.tabsText,
{ color: isActive ? '#fff' : '#000' },
];
return (
<TouchableOpacity
onPress={() => handleTabPress(type)}
style={styles.tabs}
key={type}
>
<LinearGradient
colors={gradientColors}
start={{ x: 0.5, y: 1 }}
end={{ x: 1.5, y: 1 }}
style={gradientStyle}
>
<Image source={logo} style={styles.tabsLogo} />
<Text style={textStyle}>{label}</Text>
</LinearGradient>
</TouchableOpacity>
);
},
[activeTab, styles, handleTabPress, gradientStyle],
);
const tabButtons = useMemo(
() => [
renderTabButton('avia', t('Avia orqali yetkazish'), AviaLogo),
renderTabButton('auto', t('Avto orqali yetkazish'), AutoLogo),
],
[renderTabButton],
);
return <View style={styles.tabsContainer}>{tabButtons}</View>;
};
export default Tabs;

View File

@@ -0,0 +1,187 @@
import Clipboard from '@react-native-clipboard/clipboard';
import { useQuery } from '@tanstack/react-query';
import { authApi } from 'api/auth';
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
Alert,
FlatList,
StyleSheet,
Text,
TouchableOpacity,
useWindowDimensions,
View,
} from 'react-native';
import Toast from 'react-native-toast-message';
import AntDesign from 'react-native-vector-icons/AntDesign';
import Copy from 'svg/Copy';
import Kitay from 'svg/Ki';
const TabsAuto = () => {
const { width: screenWidth } = useWindowDimensions();
const scale = screenWidth < 360 ? 0.85 : 1;
const cardWidth = screenWidth * 0.95;
const { t } = useTranslation();
const styles = makeStyles(scale, cardWidth, screenWidth);
const { data: getMe } = useQuery({
queryKey: ['getMe'],
queryFn: authApi.getMe,
});
console.log(getMe);
const addressList = [
{
id: 1,
title: 'China (Auto)',
postCode: '510440',
addressInfo: [
`收货人: ${getMe?.aviaCargoId}`,
'手机号码: 18335530701',
'北京市顺义区南法信旭辉空港中心C座',
`1004 ${getMe?.aviaCargoId}`,
],
},
// {
// id: 2,
// title: 'Korea (Auto)',
// postCode: '520550',
// addressInfo: [
// '收件人李小龙AT(M312)',
// '地址∶深圳市南山区科技园科发路',
// '18号AT(M312)',
// '电话: 13800008888',
// ],
// },
];
const handleCopy = (info: string[]) => {
if (getMe?.status === 'active') {
const textToCopy = info.join('\n');
Clipboard.setString(textToCopy);
Toast.show({
type: 'success',
text1: t('Nusxa olingan'),
text2: t('Avto manzili nusxalandi!'),
position: 'top',
visibilityTime: 2000,
});
} else {
Toast.show({
type: 'error',
text1: t('Xatolik yuz berdi!'),
text2: t('Akkaunt faol emas!'),
position: 'top',
visibilityTime: 2000,
});
}
};
return (
<FlatList
data={addressList}
horizontal
keyExtractor={item => item.id.toString()}
pagingEnabled
showsHorizontalScrollIndicator={false}
snapToInterval={cardWidth + 10} // +10: marginRight
decelerationRate="fast"
contentContainerStyle={{
paddingHorizontal: (screenWidth - cardWidth) / 2,
marginTop: 10,
}}
renderItem={({ item, index }) => {
const isLast = index === addressList.length - 1;
return (
<View style={[styles.card, { marginRight: isLast ? 0 : 10 }]}>
<View style={styles.titleCard}>
<Kitay width={24 * scale} height={24 * scale} />
<Text style={styles.title}>{item.title}</Text>
</View>
<View style={styles.infoId}>
<View style={{ gap: 4 * scale }}>
{item.addressInfo.map((line, idx) => (
<Text key={idx} style={styles.infoText}>
{line}
</Text>
))}
</View>
<TouchableOpacity onPress={() => handleCopy(item.addressInfo)}>
<Copy color="#28A7E8" width={24 * scale} height={24 * scale} />
</TouchableOpacity>
</View>
<View style={styles.postCodeWrapper}>
<Text style={styles.postCodeText}>{t('Auto post kodi')}: </Text>
<Text style={styles.postCode}>{item.postCode}</Text>
<TouchableOpacity
onPress={() => {
Clipboard.setString(item.postCode);
Alert.alert(t('Nusxa olindi'), t('Pochta kodi nusxalandi!'));
}}
style={{ marginLeft: 4 * scale }}
>
<AntDesign
name="pushpin"
color="red"
size={16 * scale}
style={{ transform: [{ rotateY: '180deg' }] }}
/>
</TouchableOpacity>
</View>
</View>
);
}}
/>
);
};
const makeStyles = (scale: number, cardWidth: number, screenWidth: number) =>
StyleSheet.create({
scrollContainer: {
marginTop: 20,
paddingHorizontal: (screenWidth - cardWidth) / 2,
},
card: {
height: 220 * scale,
width: cardWidth,
backgroundColor: '#28a8e82c',
borderRadius: 12 * scale,
padding: 15 * scale,
gap: 10 * scale,
},
titleCard: {
flexDirection: 'row',
gap: 8 * scale,
alignItems: 'center',
},
title: {
fontSize: 20 * scale,
fontWeight: '600',
color: '#101623CC',
},
infoId: {
flexDirection: 'row',
justifyContent: 'space-between',
marginVertical: 8 * scale,
},
infoText: {
fontSize: 16 * scale,
color: '#28A7E8',
},
postCodeWrapper: {
flexDirection: 'row',
alignItems: 'center',
},
postCodeText: {
fontSize: 16 * scale,
color: '#000000',
fontWeight: '500',
},
postCode: {
fontSize: 16 * scale,
color: '#28A7E8',
fontWeight: '400',
marginLeft: 4 * scale,
},
});
export default TabsAuto;

View File

@@ -0,0 +1,195 @@
import Clipboard from '@react-native-clipboard/clipboard';
import { useQuery } from '@tanstack/react-query';
import { authApi } from 'api/auth';
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
Alert,
FlatList,
StyleSheet,
Text,
TouchableOpacity,
useWindowDimensions,
View,
} from 'react-native';
import Toast from 'react-native-toast-message';
import AntDesign from 'react-native-vector-icons/AntDesign';
import Copy from 'svg/Copy';
import Kitay from 'svg/Ki';
const TabsAvia = () => {
const { data: getMe, isLoading: getMeLoad } = useQuery({
queryKey: ['getMe'],
queryFn: authApi.getMe,
});
const addressList = [
{
id: 1,
title: 'China (Avia)',
postCode: '510440',
addressInfo: [
`收货人: ${getMe?.aviaCargoId}`,
'手机号码: 18335530701',
'北京市顺义区南法信旭辉空港中心C座',
`1004 ${getMe?.aviaCargoId}`,
],
},
// {
// id: 2,
// title: 'Korea (Avia)',
// postCode: '510440',
// addressInfo: [
// '收货人: M312',
// '手机号码: 18335530701',
// '北京市顺义区南法信旭辉空港中心C座',
// '1004 N209',
// ],
// },
];
const { width: screenWidth } = useWindowDimensions();
const scale = screenWidth < 360 ? 0.85 : 1;
const cardWidth = screenWidth * 0.95;
const styles = makeStyles(scale, cardWidth, screenWidth);
const { t } = useTranslation();
const handleCopy = (info: string[]) => {
if (getMe?.status === 'active') {
const textToCopy = info.join('\n');
Clipboard.setString(textToCopy);
Toast.show({
type: 'success',
text1: t('Nusxa olingan'),
text2: t('Avia manzili nusxalandi!'),
position: 'top',
visibilityTime: 2000,
});
} else {
Toast.show({
type: 'error',
text1: t('Xatolik yuz berdi!'),
text2: t('Akkaunt faol emas!'),
position: 'top',
visibilityTime: 2000,
});
}
};
return (
<FlatList
data={addressList}
horizontal
keyExtractor={item => item.id.toString()}
pagingEnabled
showsHorizontalScrollIndicator={false}
snapToInterval={cardWidth + 10} // +10: marginRight
decelerationRate="fast"
contentContainerStyle={{
paddingHorizontal: (screenWidth - cardWidth) / 2,
marginTop: 20,
}}
renderItem={({ item, index }) => {
const isLast = index === addressList.length - 1;
return (
<View style={[styles.card, { marginRight: isLast ? 0 : 10 }]}>
<View style={styles.titleCard}>
<Kitay width={24 * scale} height={24 * scale} />
<Text style={styles.title}>{item.title}</Text>
</View>
<View style={styles.infoId}>
<View style={{ gap: 4 * scale }}>
{item.addressInfo.map((line, idx) => (
<Text key={idx} style={styles.infoText}>
{line}
</Text>
))}
</View>
<TouchableOpacity onPress={() => handleCopy(item.addressInfo)}>
<Copy color="#28A7E8" width={24 * scale} height={24 * scale} />
</TouchableOpacity>
</View>
<View style={styles.postCodeWrapper}>
<Text style={styles.postCodeText}>{t('Avia post kodi')}: </Text>
<Text style={styles.postCode}>{item.postCode}</Text>
<TouchableOpacity
onPress={() => {
Clipboard.setString(item.postCode);
Alert.alert(t('Nusxa olindi'), t('Pochta kodi nusxalandi!'));
}}
style={{ marginLeft: 4 * scale }}
>
<AntDesign
name="pushpin"
color="red"
size={16 * scale}
style={{ transform: [{ rotateY: '180deg' }] }}
/>
</TouchableOpacity>
</View>
</View>
);
}}
/>
);
};
const makeStyles = (scale: number, cardWidth: number, screenWidth: number) =>
StyleSheet.create({
container: {
height: 200,
width: '95%',
backgroundColor: '#28a8e82c',
margin: 'auto',
marginTop: 20,
borderRadius: 12,
padding: 12,
gap: 10,
},
scrollContainer: {
marginTop: 20,
paddingHorizontal: (screenWidth - cardWidth) / 2,
},
postCodeWrapper: {
flexDirection: 'row',
alignItems: 'center',
},
card: {
height: 220 * scale,
width: cardWidth,
backgroundColor: '#28a8e82c',
borderRadius: 12 * scale,
padding: 15 * scale,
gap: 10 * scale,
},
titleCard: {
flexDirection: 'row',
gap: 8,
alignItems: 'center',
},
title: {
fontSize: 20,
fontWeight: '600',
color: '#101623CC',
},
infoId: {
flexDirection: 'row',
justifyContent: 'space-between',
},
infoText: {
fontSize: 16,
color: '#28A7E8',
},
postCodeText: {
fontSize: 16,
color: '#000000',
fontWeight: '500',
},
postCode: {
fontSize: 16,
color: '#28A7E8',
fontWeight: '400',
},
});
export default TabsAvia;

View File

@@ -0,0 +1,182 @@
import { StyleSheet } from 'react-native';
export const HomeStyle = (scale: number) =>
StyleSheet.create({
container: {
flex: 1,
},
header: {
backgroundColor: '#28A7E8',
height: 80 * scale,
paddingHorizontal: 10 * scale,
paddingVertical: 10 * scale,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
title: {
color: '#fff',
fontSize: 20 * scale,
},
logo: {
flexDirection: 'row',
alignItems: 'center',
gap: 5 * scale,
},
links: {
flexDirection: 'row',
alignItems: 'center',
gap: 15 * scale,
},
autoContainer: {
width: '90%',
height: 140 * scale,
borderRadius: 8 * scale,
padding: 10 * scale,
backgroundColor: '#FFFFFF',
marginTop: 10,
gap: 8 * scale,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 * scale },
shadowOpacity: 0.1,
shadowRadius: 2 * scale,
margin: 'auto',
elevation: 2,
},
autoCard: {
flexDirection: 'row',
justifyContent: 'space-between',
},
reysTitle: {
fontSize: 20 * scale,
color: '#28A7E8',
fontWeight: '600',
},
text: {
fontWeight: '500',
color: 'red',
fontSize: 14 * scale,
},
box: {
backgroundColor: '#28A7E81A',
padding: 10 * scale,
width: 'auto',
alignSelf: 'flex-start',
borderRadius: 8 * scale,
},
date: {
backgroundColor: '#28A7E8',
padding: 6 * scale,
width: '13%',
textAlign: 'center',
justifyContent: 'center',
alignItems: 'center',
borderRadius: 8 * scale,
},
dateLabel: {
fontSize: 14 * scale,
fontWeight: '500',
color: '#FFFFFF',
},
cardHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
},
logoIcon: {
width: 50 * scale,
height: 50 * scale,
resizeMode: 'contain',
},
bellWrapper: {
position: 'relative',
},
bellDot: {
width: 10 * scale,
height: 10 * scale,
position: 'absolute',
backgroundColor: 'red',
right: 2 * scale,
borderRadius: 100,
},
divider: {
height: 2 * scale,
width: '100%',
backgroundColor: '#28A7E81F',
},
cardBody: {
flexDirection: 'row',
justifyContent: 'space-between',
},
infoBlock: {
alignItems: 'center',
flexDirection: 'row',
gap: 6 * scale,
},
iconBox: {
backgroundColor: '#28A7E81A',
padding: 8 * scale,
borderRadius: 8 * scale,
},
infoTextBlock: {
flexDirection: 'column',
},
infoTitle: {
fontWeight: '500',
fontSize: 18 * scale,
},
infoSubtext: {
fontWeight: '500',
fontSize: 14 * scale,
color: '#00000066',
},
subtextRight: {
textAlign: 'right',
},
highlightBox: {
backgroundColor: '#69ec6d9c',
alignSelf: 'flex-start',
padding: 4 * scale,
borderRadius: 8 * scale,
},
highlightText: {
fontSize: 16 * scale,
},
animatedIconWrapper: {
width: '80%',
},
row: {
flexDirection: 'row',
gap: 10 * scale,
width: '100%',
},
rowFull: {
flexDirection: 'row',
gap: 2 * scale,
width: '100%',
},
tabs: {
width: '50%',
borderRadius: 8 * scale,
justifyContent: 'space-between',
gap: 10 * scale,
},
tabsContainer: {
marginLeft: 'auto',
paddingLeft: 8 * scale,
paddingRight: 18 * scale,
flexDirection: 'row',
gap: 8 * scale,
},
tabsLogo: {
width: '40%',
height: '50%',
objectFit: 'contain',
},
tabsText: {
width: '60%',
color: '#FFFFFF',
fontSize: 16 * scale,
fontWeight: '600',
},
});

View File

@@ -0,0 +1,314 @@
import NavbarBack from 'components/NavbarBack';
import Navigation from 'components/Navigation';
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import { ScrollView, StyleSheet, Text, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import Battery from 'svg/Battery';
import Blade from 'svg/Blade';
import Book from 'svg/Book';
import Dron from 'svg/Dron';
import Drug from 'svg/Drug';
import Food from 'svg/Food';
import Glass from 'svg/Glass';
import Jewelry from 'svg/Jewelry';
import Pencil from 'svg/Pencil';
import WaterGlass from 'svg/WaterGlass';
import Tabs from '../../home/ui/Tabs';
interface RestrictedProductProps {}
const RestrictedProduct = (props: RestrictedProductProps) => {
const [activeTab, setActiveTab] = React.useState<'avia' | 'auto'>('avia');
const { t } = useTranslation();
return (
<SafeAreaView style={{ flex: 1 }}>
<NavbarBack title={t('Taqiqlangan buyumlar')} />
<ScrollView style={{ flex: 1 }}>
<View style={styles.container}>
<Tabs activeTab={activeTab} setActiveTab={setActiveTab} />
{activeTab === 'avia' && (
<View style={{ marginTop: 20, gap: 10, marginBottom: 20 }}>
<Text
style={{
width: '95%',
margin: 'auto',
fontSize: 20,
fontWeight: '500',
}}
>
{t('Aviada taqiqlangan buyumlar')}
</Text>
<View style={styles.cardWhite}>
<View style={styles.priceCard}>
<Text style={styles.titleBlack}>
<WaterGlass />
</Text>
<Text style={[styles.titleBlack, { fontSize: 16 }]}>
{t('Ichida suyuqligi bor narsalar')}
</Text>
</View>
</View>
<View style={styles.cardWhite}>
<View style={styles.priceCard}>
<Text style={styles.titleBlack}>
<Battery />
</Text>
<View style={{ width: '90%' }}>
<Text style={[styles.titleBlack, { fontSize: 16 }]}>
{t('Batareykasi va magnit bolgan istalgan narsa')}
</Text>
<Text style={styles.desc}>
{t(
'(Telifon, sensitive buyumlar, airpods, naushnik, qol soati, tagi yonadigan krasovkalar...)',
)}
</Text>
</View>
</View>
</View>
<View style={styles.cardWhite}>
<View style={styles.priceCard}>
<Text style={styles.titleBlack}>
<Pencil />
</Text>
<View style={{ width: '90%' }}>
<Text style={[styles.titleBlack, { fontSize: 16 }]}>
{t('Kukunli buyumlar')}
</Text>
<Text style={styles.desc}>{t('(Pudra, ten...)')}</Text>
</View>
</View>
</View>
<View style={styles.cardWhite}>
<View style={styles.priceCard}>
<Text style={styles.titleBlack}>
<Glass />
</Text>
<View style={{ width: '90%' }}>
<Text style={[styles.titleBlack, { fontSize: 16 }]}>
{t('Parfumeriya')}
</Text>
<Text style={styles.desc}>
{t(
'(Barcha Parfumeriya va kosmetika, yuvinish maxsulotlari)',
)}
</Text>
</View>
</View>
</View>
<View style={styles.cardWhite}>
<View style={styles.priceCard}>
<Text style={styles.titleBlack}>
<Blade />
</Text>
<View style={{ width: '90%' }}>
<Text style={[styles.titleBlack, { fontSize: 16 }]}>
{t('Otkir tigli va sovuq qirollar')}
</Text>
<Text style={styles.desc}>
{t('(Pichoq, qaychi, miltiq...)')}
</Text>
</View>
</View>
</View>
<View style={styles.cardWhite}>
<View style={styles.priceCard}>
<Text style={styles.titleBlack}>
<Jewelry />
</Text>
<View style={{ width: '90%' }}>
<Text style={[styles.titleBlack, { fontSize: 16 }]}>
{t('Zargarklik buyumlari')}
</Text>
<Text style={styles.desc}>
{t('(Tilla, kumush, olmos, braslit...)')}
</Text>
</View>
</View>
</View>
<View style={styles.cardWhite}>
<View style={styles.priceCard}>
<Text style={styles.titleBlack}>
<Drug />
</Text>
<View style={{ width: '90%' }}>
<Text style={[styles.titleBlack, { fontSize: 16 }]}>
{t('Dori darmon va med texnika')}
</Text>
</View>
</View>
</View>
<View style={styles.cardWhite}>
<View style={styles.priceCard}>
<Text style={styles.titleBlack}>
<Food />
</Text>
<View style={{ width: '90%' }}>
<Text style={[styles.titleBlack, { fontSize: 16 }]}>
{t('Oziq ovqat')}
</Text>
</View>
</View>
</View>
<View style={[styles.card]}>
<Text style={[styles.titleBlack, { color: '#000000B2' }]}>
{t(
"Agar sizda g'ayrioddiy yoki noaniq mahsulot bo'lsa, albatta buyurtma qilishdan oldin so'rashingiz tavsiya etiladi.",
)}
</Text>
</View>
<View style={[styles.card]}>
<Text style={[styles.titleBlack, { color: '#FF6363' }]}>
{t('Muhim!')}
</Text>
</View>
<View style={[styles.card]}>
<Text style={[styles.titleBlack, { color: '#000000B2' }]}>
{t(
"Avia pochta manzili orqali yuborilishi taqiqlangan mahsulot buyurtma qilgan bo'lsangiz, u avtomatik ravishda avtokargo yukiga (avto) o'tkaziladi. Shunday qilib, yukingiz Xitoy omborida qolib ketmaydi.",
)}
</Text>
</View>
<View style={[styles.cardWhite, { backgroundColor: '#DFF2FD' }]}>
<Text style={[styles.desc, { color: '#28A7E8B2' }]}>
{t(
`Shu bilan birga, Aviada ham, Avtoda ham taqiqlangan mahsulot yuborilgan bo'lsa bunday holatda mahsulot O'zbekistonga yuborilmaydi va bu uchun javobgarlik mijozga yuklanadi.`,
)}
</Text>
</View>
</View>
)}
{activeTab === 'auto' && (
<View style={{ marginTop: 20, gap: 10, marginBottom: 20 }}>
<View style={styles.cardWhite}>
<View style={styles.priceCard}>
<Text style={styles.titleBlack}>
<Jewelry />
</Text>
<View style={{ width: '90%' }}>
<Text style={[styles.titleBlack, { fontSize: 16 }]}>
{t('Zargarklik buyumlari')}
</Text>
<Text style={styles.desc}>
{t('(Tilla, kumush, olmos, braslit...)')}
</Text>
</View>
</View>
</View>
<View style={styles.cardWhite}>
<View style={styles.priceCard}>
<Text style={styles.titleBlack}>
<Drug />
</Text>
<View style={{ width: '90%' }}>
<Text style={[styles.titleBlack, { fontSize: 16 }]}>
{t('Dori darmon va med texnika')}
</Text>
</View>
</View>
</View>
<View style={styles.cardWhite}>
<View style={styles.priceCard}>
<Text style={styles.titleBlack}>
<Food />
</Text>
<View style={{ width: '90%' }}>
<Text style={[styles.titleBlack, { fontSize: 16 }]}>
{t('Oziq ovqat')}
</Text>
</View>
</View>
</View>
<View style={styles.cardWhite}>
<View style={styles.priceCard}>
<Text style={styles.titleBlack}>
<Book />
</Text>
<View style={{ width: '90%' }}>
<Text style={[styles.titleBlack, { fontSize: 16 }]}>
{t('Diniy kitob va diniy buyumlar')}
</Text>
</View>
</View>
</View>
<View style={styles.cardWhite}>
<View style={styles.priceCard}>
<Text style={styles.titleBlack}>
<Dron />
</Text>
<View style={{ width: '90%' }}>
<Text style={[styles.titleBlack, { fontSize: 16 }]}>
{t('Dron, avtomat qurollar')}
</Text>
</View>
</View>
</View>
<View style={[styles.card]}>
<Text style={[styles.titleBlack, { color: '#000000B2' }]}>
{t(
"Agar sizda g'ayrioddiy yoki noaniq mahsulot bo'lsa, albatta buyurtma qilishdan oldin so'rashingiz tavsiya etiladi.",
)}
</Text>
</View>
<View style={[styles.card]}>
<Text style={[styles.titleBlack, { color: '#FF6363' }]}>
{t('Muhim!')}
</Text>
</View>
<View style={[styles.card]}>
<Text style={[styles.titleBlack, { color: '#000000B2' }]}>
{t(
"Avia pochta manzili orqali yuborilishi taqiqlangan mahsulot buyurtma qilgan bo'lsangiz, u avtomatik ravishda avtokargo yukiga (avto) o'tkaziladi. Shunday qilib, yukingiz Xitoy omborida qolib ketmaydi.",
)}
</Text>
</View>
<View style={[styles.cardWhite, { backgroundColor: '#DFF2FD' }]}>
<Text style={[styles.desc, { color: '#28A7E8B2' }]}>
{t(
"Shu bilan birga, Aviada ham, Avtoda ham taqiqlangan mahsulot yuborilgan bo'lsa bunday holatda mahsulot O'zbekistonga yuborilmaydi va bu uchun javobgarlik mijozga yuklanadi.",
)}
</Text>
</View>
</View>
)}
</View>
</ScrollView>
<Navigation />
</SafeAreaView>
);
};
export default RestrictedProduct;
const styles = StyleSheet.create({
container: {
marginTop: 20,
},
card: {
width: '95%',
gap: 5,
margin: 'auto',
},
cardWhite: {
backgroundColor: '#FFFFFF',
width: '95%',
gap: 5,
margin: 'auto',
padding: 10,
borderRadius: 8,
},
titleBlack: {
fontSize: 16,
fontWeight: '500',
},
desc: {
color: '#000000B2',
fontSize: 14,
fontWeight: '400',
},
priceCard: {
flexDirection: 'row',
gap: 10,
alignItems: 'flex-start',
},
});

View File

@@ -0,0 +1,72 @@
import Navbar from 'components/Navbar';
import Navigation from 'components/Navigation';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Image, ScrollView, StyleSheet, Text, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
const Uncodified = () => {
const { t } = useTranslation();
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={styles.container}>
<Navbar />
<ScrollView style={{ marginTop: 20 }}>
<View style={styles.card}>
<Image
src="https://jebnalak.com/cdn/shop/products/Untitled-2021-09-12T112500.494_370x370.jpg?v=1631435110"
width={200}
height={200}
style={styles.image}
/>
<View style={styles.textCard}>
<Text style={styles.title}>{t('Trek ID')}:</Text>
<Text style={styles.text}>YT12345678</Text>
</View>
</View>
</ScrollView>
<Navigation />
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
card: {
backgroundColor: '#FFFFFF',
padding: 20,
borderRadius: 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 4,
marginBottom: 20,
width: '95%',
margin: 'auto',
},
image: {
width: '100%',
height: 200,
objectFit: 'contain',
},
textCard: {
marginTop: 10,
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
},
title: {
fontSize: 16,
fontWeight: '500',
},
text: {
fontSize: 14,
fontWeight: '500',
},
});
export default Uncodified;

View File

@@ -0,0 +1,12 @@
import z from 'zod';
export const CreatePassSchema = 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"),
passportSeriya: z.string().length(2, '2 ta harf kerak'),
passportNumber: z.string().length(7, '7 ta raqam kerak'),
jshshir: z.string().length(14, '14 ta raqam kerak'),
birthDate: z.string().min(8, 'Majburiy maydon'),
});
export type CreatePassSchemaType = z.infer<typeof CreatePassSchema>;

View File

@@ -0,0 +1,234 @@
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import LottieView from 'lottie-react-native';
import React, { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Animated,
StyleSheet,
Text,
TouchableOpacity,
useWindowDimensions,
View,
} from 'react-native';
import Clock from 'screens/../../assets/lottie/Sand clock.json';
import ProgressBar from 'screens/../../assets/lottie/Success.json';
import Warning from 'screens/../../assets/lottie/Warning animation.json';
import CloseIcon from 'svg/Close';
import { RootStackParamList } from 'types/types';
type NavigationProp = NativeStackNavigationProp<
RootStackParamList,
'PaymentMethod'
>;
interface ModalSuccessViewProps {
visible: boolean;
error: boolean;
setVisible: React.Dispatch<React.SetStateAction<boolean>>;
}
const CreateModal = ({ visible, setVisible, error }: ModalSuccessViewProps) => {
const navigation = useNavigation<NavigationProp>();
const { t } = useTranslation();
const [successMet, setSuccessMet] = useState(false);
const opacity = useRef(new Animated.Value(0)).current;
const translateY = useRef(new Animated.Value(50)).current;
const { width } = useWindowDimensions();
const scale = width < 360 ? 0.8 : 1;
const closeModal = () => {
Animated.timing(opacity, {
toValue: 0,
duration: 200,
useNativeDriver: true,
}).start(() => {
setVisible(false);
navigation.navigate('Passports');
});
};
useEffect(() => {
if (visible) {
Animated.parallel([
Animated.timing(opacity, {
toValue: 1,
duration: 250,
useNativeDriver: true,
}),
Animated.spring(translateY, {
toValue: 0,
useNativeDriver: true,
}),
]).start();
const timer = setTimeout(() => setSuccessMet(true), 3000);
return () => clearTimeout(timer);
} else {
setSuccessMet(false);
}
}, [visible]);
if (!visible) return null;
return (
<View style={styles.overlay}>
<Animated.View
style={[
styles.modalContent,
{
opacity: opacity,
transform: [{ translateY }],
},
]}
>
<View style={styles.header}>
<Text style={styles.title}>
{error
? t("Passport qo'shishda xatolik yuz berdi")
: t("Passport muvaffaqqiyatli qo'shildi")}
</Text>
<TouchableOpacity
onPress={closeModal}
style={styles.closeBtn}
disabled={!successMet}
>
<CloseIcon width={15} height={15} color={'#000000'} />
</TouchableOpacity>
</View>
<View style={styles.divider} />
<View style={styles.content}>
{successMet ? (
<View style={styles.content}>
{error ? (
<LottieView
source={Warning}
loop
autoPlay={true}
resizeMode="cover"
style={{ width: 100 * scale, height: 100 * scale }}
/>
) : (
<LottieView
source={ProgressBar}
loop
autoPlay={true}
resizeMode="cover"
style={{ width: 100 * scale, height: 100 * scale }}
/>
)}
</View>
) : (
<LottieView
source={Clock}
loop
autoPlay={true}
resizeMode="cover"
style={{ width: 100 * scale, height: 100 * scale }}
/>
)}
</View>
{successMet && (
<>
{error ? (
<TouchableOpacity
style={styles.btn}
onPress={() => setVisible(false)}
>
<Text style={styles.btnText}>{t('Yaxshi')}</Text>
</TouchableOpacity>
) : (
<TouchableOpacity style={styles.btn} onPress={closeModal}>
<Text style={styles.btnText}>{t('Yaxshi')}</Text>
</TouchableOpacity>
)}
</>
)}
</Animated.View>
</View>
);
};
const styles = StyleSheet.create({
overlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0,0,0,0.4)',
justifyContent: 'center',
alignItems: 'center',
zIndex: 1000,
},
modalContent: {
width: '90%',
backgroundColor: '#fff',
borderRadius: 10,
paddingBottom: 20,
shadowColor: '#000',
shadowOpacity: 0.3,
shadowRadius: 4,
elevation: 6,
},
header: {
padding: 20,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
title: {
fontSize: 16,
fontWeight: '500',
},
closeBtn: {
padding: 5,
width: 30,
height: 30,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#0000000D',
borderRadius: 50,
},
divider: {
height: 1,
backgroundColor: '#E0E0E0',
marginBottom: 10,
},
content: {
alignItems: 'center',
gap: 10,
paddingVertical: 20,
},
circle: {
backgroundColor: '#28A7E8',
width: 60,
height: 60,
borderRadius: 30,
justifyContent: 'center',
alignItems: 'center',
},
status: {
color: '#00000099',
fontSize: 16,
fontWeight: '500',
},
btn: {
alignSelf: 'center',
height: 50,
width: '90%',
borderRadius: 8,
justifyContent: 'center',
backgroundColor: '#28A7E8',
marginTop: 10,
},
btnText: {
textAlign: 'center',
color: '#fff',
fontSize: 18,
},
});
export default CreateModal;

View File

@@ -0,0 +1,404 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation } from '@tanstack/react-query';
import passportApi, { AddPassportPayload } from 'api/passport';
import DatePickerInput from 'components/DatePicker';
import SingleFileDrop from 'components/FileDrop';
import NavbarBack from 'components/NavbarBack';
import Navigation from 'components/Navigation';
import React, { useEffect, useRef, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import {
ActivityIndicator,
Dimensions,
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 { PassportStyle } from '../../myPassport/ui/styled';
import { CreatePassSchema, CreatePassSchemaType } from '../lib/form';
import CreateModal from './CreateModal';
interface FileData {
uri: string;
name: string;
type: string;
base64: string;
}
const CreatePassword = () => {
const { t } = useTranslation();
const windowWidth = Dimensions.get('window').width;
const isSmallScreen = windowWidth < 300;
const passportNumberRef = useRef<TextInput>(null);
const [inputValue, setInputValue] = useState('');
const [isDatePickerVisible, setDatePickerVisibility] = useState(false);
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
const [success, setSuccess] = React.useState(false);
const [error, setError] = useState(false);
const [frontImage, setFrontImage] = useState<FileData | null>(null);
const [backImage, setBackImage] = useState<FileData | null>(null);
const { mutate, isPending } = useMutation({
mutationFn: (payload: AddPassportPayload) => {
const res = passportApi.addPassport(payload);
return res;
},
onSuccess: res => {
setSuccess(true);
},
onError: err => {
console.dir(err);
setError(true);
},
});
useEffect(() => {
if (isPending) {
setSuccess(true);
}
}, [isPending]);
const {
control,
handleSubmit,
setValue,
formState: { errors },
getValues,
} = useForm<CreatePassSchemaType>({
resolver: zodResolver(CreatePassSchema),
defaultValues: {
firstName: '',
lastName: '',
birthDate: '',
passportSeriya: '',
passportNumber: '',
jshshir: '',
},
});
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 onSubmit = (data: CreatePassSchemaType) => {
const [d, m, y] = data.birthDate.split('/');
const isoBirthDate = `${y}-${m.padStart(2, '0')}-${d.padStart(2, '0')}`;
mutate({
fullName: `${data.firstName} ${data.lastName}`,
birthDate: isoBirthDate,
passportSerial: `${data.passportSeriya.toUpperCase()}${
data.passportNumber
}`,
passportPin: data.jshshir,
passportFrontImage: frontImage ? `${frontImage.base64}` : '',
passportBackImage: backImage ? `${backImage.base64}` : '',
});
};
return (
<SafeAreaView style={{ flex: 1 }}>
<NavbarBack title={t("Yangi pasport qo'shish")} />
<KeyboardAvoidingView
style={PassportStyle.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView
showsVerticalScrollIndicator={false}
style={PassportStyle.content}
>
<View style={PassportStyle.scrollContainer}>
<View style={PassportStyle.loginContainer}>
<Controller
control={control}
name="firstName"
render={({ field: { onChange, value } }) => (
<View>
<Text style={PassportStyle.label}>{t('Ism')}</Text>
<TextInput
style={PassportStyle.input}
placeholder={t('Ismingiz')}
onChangeText={onChange}
value={value}
placeholderTextColor={'#D8DADC'}
/>
{errors.firstName && (
<Text style={PassportStyle.errorText}>
{t(errors.firstName.message || '')}
</Text>
)}
</View>
)}
/>
<Controller
control={control}
name="lastName"
render={({ field: { onChange, value } }) => (
<View>
<Text style={PassportStyle.label}>{t('Familiya')}</Text>
<TextInput
style={PassportStyle.input}
placeholder={t('Familiyangiz')}
placeholderTextColor={'#D8DADC'}
onChangeText={onChange}
value={value}
/>
{errors.lastName && (
<Text style={PassportStyle.errorText}>
{t(errors.lastName.message || '')}
</Text>
)}
</View>
)}
/>
<View>
<Text style={PassportStyle.label}>
{t('Passport seriya raqami')}
</Text>
<View style={{ flexDirection: 'row' }}>
<Controller
control={control}
name="passportSeriya"
render={({ field: { onChange, value } }) => (
<TextInput
style={[PassportStyle.input, PassportStyle.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={[PassportStyle.input, PassportStyle.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={PassportStyle.errorText}>
{t(errors.passportSeriya?.message || '') ||
t(errors.passportNumber?.message || '')}
</Text>
)}
</View>
<View>
<Text style={PassportStyle.label}>{t('JSHSHIR')}</Text>
<Controller
control={control}
name="jshshir"
render={({ field: { onChange, value } }) => (
<TextInput
style={PassportStyle.input}
placeholder="12345678901234"
placeholderTextColor={'#D8DADC'}
keyboardType="numeric"
maxLength={14}
value={value}
onChangeText={text => {
const onlyNumbers = text.replace(/[^0-9]/g, '');
onChange(onlyNumbers);
}}
/>
)}
/>
{errors.jshshir && (
<Text style={PassportStyle.errorText}>
{t(errors.jshshir.message || '')}
</Text>
)}
</View>
<View>
<Controller
control={control}
name="birthDate"
render={({ field: { onChange, value } }) => (
<View style={{ marginBottom: 10 }}>
<Text style={PassportStyle.label}>
{t("Tug'ilgan sana")}
</Text>
<View style={[PassportStyle.inputContainer]}>
<TextInput
style={[
PassportStyle.input,
{ flex: 1, borderWidth: 0, padding: 0 },
]}
placeholder="dd/mm/yyyy"
placeholderTextColor="#D8DADC"
keyboardType="numeric"
value={value}
onChangeText={text => {
let cleaned = text
.replace(/[^\d]/g, '')
.slice(0, 8);
let formatted = '';
if (cleaned.length >= 1) {
const firstDigit = cleaned[0];
if (firstDigit > '3') return;
}
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;
}
if (cleaned.length >= 4) {
const month = parseInt(cleaned.slice(2, 4), 10);
if (month > 12 || month === 0) return;
}
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)}`;
}
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)}
>
<AntDesign
name="calendar"
color="#D8DADC"
size={25}
/>
</TouchableOpacity>
</View>
{errors.birthDate && (
<Text style={PassportStyle.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()}
/>
</View>
<View>
<Text style={PassportStyle.mainTitle}>
{t('Passport/ID karta rasmi yoki faylni yuklang')}
</Text>
<View
style={[
PassportStyle.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>
</View>
<TouchableOpacity
onPress={handleSubmit(onSubmit)}
style={PassportStyle.button}
>
{isPending ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={PassportStyle.btnText}>{t("Qo'shish")}</Text>
)}
</TouchableOpacity>
</View>
</View>
</ScrollView>
</KeyboardAvoidingView>
{success && (
<CreateModal visible={success} setVisible={setSuccess} error={error} />
)}
<Navigation />
</SafeAreaView>
);
};
export default CreatePassword;

View File

@@ -0,0 +1,30 @@
interface PassportData {
id: string; // Tez ID
jshshir: string; // JSHSHIR
fullName: string; // Toliq ism
passport: string; // Pasport raqami
birthDate: string; // Tugilgan sana (YYYY-MM-DD)
phone: string; // Telefon raqami
limit: string; // Limit (pul birligi bilan)
}
export const fakeDataPassport: PassportData[] = [
{
id: 'TOS-76809',
jshshir: '71765421818273',
fullName: "Samandar Turgunboyev Lufullo o'g'li",
passport: 'AF8787889',
birthDate: '2003-09-04',
phone: '+998 (90) 123-45-67',
limit: '110$',
},
{
id: 'TOS-98765',
jshshir: '71765421818275',
fullName: 'Jasur Rahmatov',
passport: 'AF7654321',
birthDate: '1985-12-20',
phone: '+998 (93) 456-78-90',
limit: '200$',
},
];

View File

@@ -0,0 +1,203 @@
import Clipboard from '@react-native-clipboard/clipboard';
import { getMeData } from 'api/auth/type';
import { myPassport } from 'api/passport';
import formatDate from 'helpers/formatData';
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
Dimensions,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import Toast from 'react-native-toast-message';
import Copy from 'svg/Copy';
const screenWidth = Dimensions.get('window').width;
const isSmallScreen = screenWidth < 200;
interface Props {
myPassport: myPassport[];
getMe: getMeData;
}
const MyPassport = ({ getMe, myPassport }: Props) => {
const { t } = useTranslation();
const handleCopy = (text: string) => {
Clipboard.setString(text);
Toast.show({
type: 'success',
text1: t('Nusxa olingan'),
text2: t('Pochta kodi nusxalandi!'),
position: 'top',
visibilityTime: 2000,
});
};
return (
<View style={styles.container}>
{myPassport &&
myPassport.map(data => (
<View style={styles.card} key={data.passportPin}>
<Text style={styles.title}>{t('Passport malumotlarim')}</Text>
<View style={styles.infoCard}>
<View
style={[
styles.info,
isSmallScreen
? { flexBasis: '100%', alignItems: 'flex-start' }
: { flexBasis: '35%', alignItems: 'flex-start' },
]}
>
<View style={styles.infoHeader}>
<Text style={styles.infoTitle}>{t('Tez ID')}</Text>
<TouchableOpacity
onPress={() =>
handleCopy((getMe && getMe?.aviaCargoId) || '')
}
>
<Copy color="#28A7E8" width={20} height={20} />
</TouchableOpacity>
</View>
<Text style={styles.infoText}>{getMe?.aviaCargoId}</Text>
</View>
<View
style={[
styles.info,
isSmallScreen
? { flexBasis: '100%', alignItems: 'flex-start' }
: { flexBasis: '65%', alignItems: 'flex-end' },
]}
>
<Text style={styles.infoTitle}>{t('JSHSHIR')}</Text>
<Text style={styles.infoText}>{data.passportPin}</Text>
</View>
<View
style={[
styles.info,
{ flexBasis: '100%', alignItems: 'flex-start' },
]}
>
<Text style={styles.infoTitle}>{t('Toliq ismi')}</Text>
<Text style={styles.infoText}>{data.fullName}</Text>
</View>
<View
style={[
styles.info,
isSmallScreen
? { flexBasis: '100%', alignItems: 'flex-start' }
: { flexBasis: '48%', alignItems: 'flex-start' },
]}
>
<Text style={styles.infoTitle}>{t('Passport seriya')}</Text>
<Text style={styles.infoText}>{data.passportSeries}</Text>
</View>
<View
style={[
styles.info,
isSmallScreen
? { flexBasis: '100%', alignItems: 'flex-start' }
: { flexBasis: '48%', alignItems: 'flex-end' },
]}
>
<Text style={styles.infoTitle}>{t('Tugilgan kun')}</Text>
<Text style={styles.infoText}>
{formatDate(data.birthDate)}
</Text>
</View>
<View
style={[
styles.info,
{ flexBasis: '100%', alignItems: 'flex-start' },
]}
>
<Text style={[styles.infoTitle]}>{t('Telefon raqami')}</Text>
<Text style={[styles.infoText]}>+{data.phone}</Text>
</View>
<View
style={[
styles.info,
{ flexBasis: '100%', alignItems: 'flex-end' },
]}
>
<Text
style={[
styles.infoTitle,
!isSmallScreen && { textAlign: 'right' },
]}
>
{t('Limit')}
</Text>
<Text
style={[
styles.infoText,
!isSmallScreen && { textAlign: 'right' },
]}
>
{data.availableLimit}$
</Text>
</View>
</View>
</View>
))}
</View>
);
};
const styles = StyleSheet.create({
container: {
width: '95%',
alignSelf: 'center',
marginTop: 10,
},
card: {
backgroundColor: '#FFFFFF',
padding: 20,
borderRadius: 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 4,
marginBottom: 20,
},
title: {
fontSize: 18,
fontWeight: '500',
color: '#343434',
marginBottom: 10,
},
infoCard: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
},
info: {
marginBottom: 15,
gap: 5,
},
infoTitle: {
color: '#979797',
fontSize: 16,
fontWeight: '500',
},
infoText: {
fontSize: 18,
fontWeight: '500',
},
infoHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: 20,
},
});
export default MyPassport;

View File

@@ -0,0 +1,195 @@
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useQuery } from '@tanstack/react-query';
import { authApi } from 'api/auth';
import passportApi from 'api/passport';
import LoadingScreen from 'components/LoadingScreen';
import Navbar from 'components/Navbar';
import Navigation from 'components/Navigation';
import NoResult from 'components/NoResult';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
RefreshControl,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
useWindowDimensions,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import PassportIcon from 'svg/Passport';
import Plus from 'svg/Plus';
import MyPassport from './MyPassport';
const Passport = () => {
const { t } = useTranslation();
const navigation = useNavigation<NativeStackNavigationProp<any>>();
const { width: screenWidth } = useWindowDimensions();
const scale = screenWidth < 360 ? 0.85 : 1;
const [refreshing, setRefreshing] = useState(false);
const {
data: myPassport,
isLoading: passportLoad,
isFetching: passFetching,
refetch: passRef,
isError: isErrorPass,
} = useQuery({
queryKey: ['myPassport'],
queryFn: passportApi.getPassport,
});
const { data: getMe, isLoading: getMeLoad } = useQuery({
queryKey: ['getMe'],
queryFn: authApi.getMe,
});
const onRefresh = useCallback(async () => {
setRefreshing(true);
await passRef();
}, [passRef]);
useEffect(() => {
if (!passportLoad && refreshing) {
setRefreshing(false);
}
}, [passportLoad, refreshing]);
const refreshControl = useMemo(
() => <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />,
[refreshing, onRefresh],
);
const handleNavigateToCreatePassword = useCallback(() => {
navigation.navigate('create-password');
}, [navigation]);
if (passportLoad || getMeLoad || passFetching) {
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={styles.container}>
<Navbar />
<LoadingScreen message="Pasport sahifasi yuklanmoqda..." />
<Navigation />
</View>
</SafeAreaView>
);
}
if (isErrorPass) {
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={styles.container}>
<Navbar />
<NoResult message="Xatolik yuz berdi" />
<Navigation />
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={styles.container}>
<Navbar />
<ScrollView
refreshControl={refreshControl}
removeClippedSubviews={true}
keyboardShouldPersistTaps="handled"
>
<View style={styles.header}>
<Text style={styles.title}>{t('Passportlarim')}</Text>
</View>
{myPassport && myPassport.length === 0 ? (
<View style={styles.content}>
<View style={styles.emptyState}>
<PassportIcon
color="#ccc"
width={80 * scale}
height={80 * scale}
/>
<Text style={styles.emptyText}>
{t("Hali pasport qo'shilmagan")}
</Text>
<Text style={styles.emptySubtext}>
{t("Yangi pasport qo'shish uchun tugmani bosing")}
</Text>
</View>
</View>
) : (
<MyPassport getMe={getMe!} myPassport={myPassport!} />
)}
</ScrollView>
<TouchableOpacity
style={styles.addButton}
onPress={handleNavigateToCreatePassword}
activeOpacity={0.7}
>
<Plus color="#fff" width={24 * scale} height={24 * scale} />
<Text style={styles.addButtonText}>
{t("Yangi pasport qo'shish")}
</Text>
</TouchableOpacity>
<Navigation />
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
header: {
paddingHorizontal: 20,
paddingTop: 5,
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#333',
},
content: {
paddingHorizontal: 20,
paddingBottom: 20,
},
emptyState: {
alignItems: 'center',
marginTop: 50,
marginBottom: 30,
},
emptyText: {
fontSize: 18,
color: '#666',
marginTop: 15,
textAlign: 'center',
},
emptySubtext: {
fontSize: 14,
color: '#999',
marginTop: 5,
textAlign: 'center',
},
addButton: {
backgroundColor: '#28A7E8',
paddingVertical: 15,
paddingHorizontal: 20,
borderRadius: 10,
marginBottom: 10,
width: '95%',
margin: 'auto',
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'center',
gap: 10,
},
addButtonText: {
color: '#fff',
fontSize: 16,
fontWeight: 'bold',
},
});
export default Passport;

View File

@@ -0,0 +1,194 @@
import { StyleSheet } from 'react-native';
export const PassportStyle = StyleSheet.create({
container: {
flex: 1,
},
scrollContainer: {
flexGrow: 1,
justifyContent: 'center',
width: '100%',
},
loginContainer: {
width: '95%',
margin: 'auto',
marginTop: 20,
borderRadius: 20,
display: 'flex',
gap: 20,
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,
marginBottom: 10,
},
inputContainer: {
flexDirection: 'row',
alignItems: 'center',
borderWidth: 1,
borderColor: '#D8DADC',
borderRadius: 12,
paddingHorizontal: 15,
height: 56,
backgroundColor: '#FFFFFF',
},
iconButton: {
position: 'absolute',
right: 20,
top: '50%',
transform: [{ translateY: -12.5 }],
},
});

View File

@@ -0,0 +1,99 @@
import LoadingScreen from 'components/LoadingScreen';
import Navbar from 'components/Navbar';
import Navigation from 'components/Navigation';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { RefreshControl, ScrollView, StyleSheet, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import ProfileHeader from './ProfileHeader';
import ProfilePages from './ProfilePages';
const Profile = () => {
const [refreshing, setRefreshing] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
const loadInitialData = async () => {
try {
await new Promise(resolve => setTimeout(resolve, 100));
setLoading(false);
} catch (error) {
console.error('Home loading error:', error);
setLoading(false);
}
};
loadInitialData();
}, []);
const onRefresh = useCallback(() => {
setRefreshing(true);
setLoading(true);
setTimeout(() => {
setRefreshing(false);
setLoading(false);
}, 100);
}, []);
const refreshControl = useMemo(
() => <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />,
[refreshing, onRefresh],
);
const contentContainerStyle = useMemo(() => ({ paddingBottom: 10 }), []);
if (loading) {
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={styles.container}>
<Navbar />
<LoadingScreen />
<Navigation />
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={styles.container}>
<Navbar />
<ScrollView
keyboardShouldPersistTaps="handled"
refreshControl={refreshControl}
contentContainerStyle={contentContainerStyle}
>
<ProfileHeader />
<ProfilePages />
</ScrollView>
<Navigation />
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
addBtn: {
backgroundColor: '#28A7E8',
padding: 10,
marginBottom: 10,
textAlign: 'center',
justifyContent: 'center',
alignItems: 'center',
width: '95%',
margin: 'auto',
borderRadius: 8,
flexDirection: 'row',
gap: 10,
position: 'static',
},
btnText: {
color: '#FFFFFF',
fontSize: 18,
fontWeight: '600',
},
});
export default Profile;

View File

@@ -0,0 +1,353 @@
import { useQuery } from '@tanstack/react-query';
import { authApi } from 'api/auth';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Dimensions,
Image,
Linking,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import ImagePicker from 'react-native-image-crop-picker';
import Modal from 'react-native-modal';
import SendIntentAndroid from 'react-native-send-intent';
import Fontisto from 'react-native-vector-icons/Fontisto';
import GalleryEdit from 'svg/GalleryEdit';
import Plus from 'svg/Plus';
import Trash from 'svg/Trash';
const { width } = Dimensions.get('window');
const isSmallScreen = width < 360;
const ProfileHeader = ({ userName = 'Samandar' }: { userName?: string }) => {
const [imageError, setImageError] = useState(true);
const { t } = useTranslation();
const [selectedImage, setSelectedImage] = useState<string>(
'https://static.vecteezy.com/system/resources/previews/019/879/186/non_2x/user-icon-on-transparent-background-free-png.png',
);
const getStatusMeta = (status?: string) => {
const key = (status || '').toLowerCase();
switch (key) {
case 'active':
return { label: t('Faol'), bg: '#E6F7EE', fg: '#1F9254' };
case 'pending':
return { label: t('Kutilmoqda'), bg: '#FFF7E6', fg: '#B26A00' };
case 'inactive':
case 'blocked':
return { label: t('Faol emas'), bg: '#FDECEF', fg: '#A61D24' };
default:
return { label: t('Faol emas'), bg: '#EDF2F7', fg: '#A61D24' };
}
};
const { data: getMe } = useQuery({
queryKey: ['getMe'],
queryFn: authApi.getMe,
});
const [isModalVisible, setModalVisible] = useState(false);
const openGallery = async () => {
try {
const image = await ImagePicker.openPicker({
width: 0,
height: 0,
cropping: true,
cropperCircleOverlay: true,
freeStyleCropEnabled: true,
hideBottomControls: false,
cropperToolbarTitle: 'Edit Image',
cropperToolbarColor: '#28A7E8',
cropperToolbarWidgetColor: '#ffffff',
cropperStatusBarColor: '#28A7E8',
cropperActiveWidgetColor: '#28A7E8',
compressImageQuality: 0.8,
mediaType: 'photo',
forceJpg: true,
});
if (image?.path) {
setSelectedImage(image.path);
setImageError(false);
setModalVisible(false);
}
} catch (error) {}
};
const handleImagePress = () => {
if (!imageError) {
setModalVisible(true);
} else {
openGallery();
}
};
const removePhoto = () => {
setSelectedImage('');
setImageError(true);
setModalVisible(false);
};
const openTelegram = async () => {
const telegramUri = 'tg://resolve?domain=cpostuz';
try {
const success = await SendIntentAndroid.openAppWithUri(telegramUri);
if (!success) Linking.openURL('https://t.me/cpostuz');
} catch (error) {
Linking.openURL('https://t.me/cpostuz');
}
};
const openInstagram = async () => {
const instagramUri = 'instagram://user?username=cpost_cargo';
try {
const success = await SendIntentAndroid.openAppWithUri(instagramUri);
if (!success) Linking.openURL('https://instagram.com/cpost_cargo');
} catch (error) {
Linking.openURL('https://instagram.com/cpost_cargo');
}
};
const openFacebook = () => {
SendIntentAndroid.openAppWithData(
'com.facebook.katana',
'fb://page/PAGE_ID',
).catch(() => Linking.openURL('https://facebook.com/'));
};
const capitalizeWords = (str: string) => {
if (!str) return '';
return str
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' ');
};
const formatPhone = (value: string) => {
const digits = value.replace(/\D/g, '');
if (digits.length === 0) return '';
const prefix = '+998 ';
let formattedNumber = prefix;
if (digits.length > 3) {
formattedNumber += `(${digits.slice(3, 5)}) `;
}
if (digits.length > 5) {
formattedNumber += digits.slice(5, 8);
}
if (digits.length > 8) {
formattedNumber += '-' + digits.slice(8, 10);
}
if (digits.length > 10) {
formattedNumber += '-' + digits.slice(10, 12);
}
return formattedNumber.trim();
};
return (
<View style={styles.container}>
<View style={styles.imageWrapper}>
{imageError ? (
<Text style={styles.fallbackText}>
{getMe?.fullName.charAt(0).toUpperCase()}
</Text>
) : (
<Image
source={{ uri: selectedImage }}
style={styles.image}
onError={() => setImageError(true)}
/>
)}
<TouchableOpacity style={styles.circle} onPress={handleImagePress}>
<Plus color="#FFFFFF" />
</TouchableOpacity>
</View>
<View style={styles.infoUser}>
<Text style={styles.name}>
{capitalizeWords(getMe?.fullName || '')}
</Text>
{getMe?.status && (
<View
style={[
styles.statusBadge,
{ backgroundColor: getStatusMeta(getMe.status).bg },
]}
>
<Text
style={[
styles.statusText,
{ color: getStatusMeta(getMe.status).fg },
]}
>
{getStatusMeta(getMe.status).label}
</Text>
</View>
)}
<Text style={styles.userId}>ID: {getMe?.aviaCargoId}</Text>
<Text style={styles.telUser}>
{getMe?.phone ? formatPhone(getMe.phone) : ''}
</Text>
</View>
<View style={styles.links}>
<TouchableOpacity onPress={openTelegram}>
<Fontisto
name="telegram"
color="#28A7E8"
size={isSmallScreen ? 20 : 24}
/>
</TouchableOpacity>
<TouchableOpacity onPress={openInstagram}>
<Fontisto
name="instagram"
color="#28A7E8"
size={isSmallScreen ? 18 : 22}
/>
</TouchableOpacity>
{/* <TouchableOpacity onPress={openFacebook}>
<MaterialIcons
name="facebook"
color="#28A7E8"
size={isSmallScreen ? 22 : 26}
/>
</TouchableOpacity> */}
</View>
<Modal
isVisible={isModalVisible}
onBackdropPress={() => setModalVisible(false)}
style={styles.modal}
>
<View style={styles.modalContent}>
<TouchableOpacity style={styles.modalBtn} onPress={openGallery}>
<GalleryEdit fill="#28A7E8" width={26} height={26} />
<Text style={styles.modalText}>{t("Rasmni o'zgartirish")}</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.modalBtn} onPress={removePhoto}>
<Trash color="red" width={26} height={26} />
<Text style={[styles.modalText, { color: 'red' }]}>
{t("Rasmni o'chirish")}
</Text>
</TouchableOpacity>
</View>
</Modal>
</View>
);
};
const styles = StyleSheet.create({
container: {
marginTop: 10,
alignItems: 'center',
},
imageWrapper: {
width: 100,
height: 100,
borderRadius: 50,
backgroundColor: '#D3D6DA',
justifyContent: 'center',
alignItems: 'center',
},
image: {
width: 100,
height: 100,
resizeMode: 'cover',
borderRadius: 50,
},
fallbackText: {
fontSize: 50,
fontWeight: 'bold',
color: '#555',
},
circle: {
position: 'absolute',
bottom: -5,
right: 0,
backgroundColor: '#28A7E8',
zIndex: 10,
borderRadius: 50,
borderColor: '#FFFFFF',
borderWidth: 2,
padding: 5,
},
statusBadge: {
alignSelf: 'center',
marginTop: 5,
marginBottom: 10,
paddingHorizontal: 14,
paddingVertical: 4,
borderRadius: 20,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.15,
shadowRadius: 3,
elevation: 3, // Android uchun chiroyli korinishi
},
statusText: {
fontSize: 13,
fontWeight: '600',
},
infoUser: {
marginTop: 10,
},
name: {
fontSize: 18,
fontWeight: '500',
},
userId: {
textAlign: 'center',
fontWeight: '600',
fontSize: 30,
color: '#28A7E8',
},
telUser: {
color: '#373737',
fontWeight: '400',
fontSize: 16,
textAlign: 'center',
},
links: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
marginTop: 10,
},
modal: {
justifyContent: 'flex-end',
margin: 0,
},
modalContent: {
backgroundColor: 'white',
padding: 20,
borderTopLeftRadius: 12,
borderTopRightRadius: 12,
},
modalBtn: {
paddingVertical: 15,
borderBottomWidth: 1,
borderBottomColor: '#eee',
flexDirection: 'row',
justifyContent: 'center',
gap: 8,
alignItems: 'center',
},
modalText: {
fontSize: 16,
fontWeight: '500',
color: '#28A7E8',
textAlign: 'center',
},
});
export default ProfileHeader;

View File

@@ -0,0 +1,179 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import {
Alert,
Linking,
Platform,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import AppLink from 'react-native-app-link';
import ArrowRightUnderline from 'svg/ArrowRightUnderline';
import Bell from 'svg/Bell';
import Location from 'svg/Location';
import Logout from 'svg/LogOut';
import Setting from 'svg/Setting';
import Support from 'svg/Supprt';
import Trash from 'svg/Trash';
interface componentNameProps {}
const ProfilePages = (props: componentNameProps) => {
const navigation = useNavigation<NativeStackNavigationProp<any>>();
const { t } = useTranslation();
const handleLogout = async () => {
Alert.alert(
t('Tasdiqlash'),
t('Haqiqatan ham profildan chiqmoqchimisiz?'),
[
{ text: t('Bekor qilish'), style: 'cancel' },
{
text: t('Chiqish'),
style: 'destructive',
onPress: async () => {
try {
await AsyncStorage.removeItem('token');
navigation.reset({
index: 0,
routes: [{ name: 'Login' }], // login sahifasiga qaytarish
});
} catch (error) {}
},
},
],
{ cancelable: true },
);
};
const openTelegram = React.useCallback(async () => {
try {
await AppLink.maybeOpenURL('tg://resolve?domain=cpostuz', {
appName: 'Telegram',
appStoreId: 686449807,
appStoreLocale: 'us',
playStoreId: 'org.telegram.messenger',
});
} catch (err) {
// Agar ilovani ham, storeni ham ochib bolmasa, fallback URL
Linking.openURL('https://t.me/cpostuz');
}
}, []);
return (
<View style={styles.container}>
<TouchableOpacity
style={[
styles.card,
{ flexDirection: 'row', alignItems: 'center', gap: 10 },
]}
onPress={() => navigation.navigate('Settings')}
>
<View style={[{ flexDirection: 'row', alignItems: 'center', gap: 10 }]}>
<Setting color="#373737" width={24} height={24} />
<Text style={styles.title}>{t('Sozlamalar')}</Text>
</View>
<ArrowRightUnderline color="#373737" width={24} height={24} />
</TouchableOpacity>
{Platform.OS === 'android' && (
<TouchableOpacity
style={[
styles.card,
{ flexDirection: 'row', alignItems: 'center', gap: 10 },
]}
onPress={() => navigation.navigate('Notifications')}
>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}>
<Bell color="#373737" width={24} height={24} />
<Text style={styles.title}>{t('Bildirishnomalar')}</Text>
</View>
<ArrowRightUnderline color="#373737" width={24} height={24} />
</TouchableOpacity>
)}
<TouchableOpacity
style={[
styles.card,
{ flexDirection: 'row', alignItems: 'center', gap: 10 },
]}
onPress={() => navigation.navigate('Warehouses')}
>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}>
<Location color="#373737" width={24} height={24} />
<Text style={styles.title}>{t('Xitoy omborlari manzili')}</Text>
</View>
<ArrowRightUnderline color="#373737" width={24} height={24} />
</TouchableOpacity>
<TouchableOpacity
style={[
styles.card,
{ flexDirection: 'row', alignItems: 'center', gap: 10 },
]}
onPress={openTelegram}
>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}>
<Support color="#373737" width={24} height={24} />
<Text style={styles.title}>{t('Yordam markazi')}</Text>
</View>
<ArrowRightUnderline color="#373737" width={24} height={24} />
</TouchableOpacity>
<View style={[styles.card, { borderBottomWidth: 0, marginTop: 10 }]}>
<TouchableOpacity
style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}
onPress={handleLogout}
>
<Logout color="#F06363" width={24} height={24} />
<Text style={[styles.title, { color: '#F06363' }]}>
{t('Chiqish')}
</Text>
</TouchableOpacity>
</View>
<View
style={[
styles.card,
{
borderBottomWidth: 0,
marginTop: 10,
alignContent: 'center',
justifyContent: 'center',
},
]}
>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}>
<Trash color="#F06363" width={24} height={24} />
<Text style={[styles.title, { color: '#F06363' }]}>
{t('Hisobingizni ochirish')}
</Text>
</View>
</View>
</View>
);
};
export default ProfilePages;
const styles = StyleSheet.create({
container: {
width: '95%',
margin: 'auto',
marginTop: 30,
},
card: {
borderBottomColor: '#D8DADC',
borderBottomWidth: 1,
paddingTop: 10,
paddingBottom: 10,
gap: 10,
flexDirection: 'row',
justifyContent: 'space-between',
},
title: {
fontSize: 16,
color: '#373737',
fontWeight: '400',
},
});

View File

@@ -0,0 +1,38 @@
export interface NotificationsData {
id: number;
title: string;
message: string;
}
export const fakeNotifications: NotificationsData[] = [
// {
// id: 1,
// title: 'Yuk yetkazildi',
// message: 'Sizning 12345 raqamli yukingiz muvaffaqiyatli yetkazildi.',
// },
// {
// id: 2,
// title: 'Yangi buyurtma',
// message: 'Yangi yuk buyurtmasi qoshildi. Buyurtma raqami: 67890.',
// },
// {
// id: 3,
// title: 'Yetkazib berish kechikdi',
// message: 'Ob-havo sababli yukingiz kechikmoqda. Tez orada yetkaziladi.',
// },
// {
// id: 4,
// title: 'Yuk jonatildi',
// message: 'Sizning 34567 raqamli yukingiz jonatildi va yolga chiqdi.',
// },
// {
// id: 5,
// title: 'Yetkazib beruvchi tayinlandi',
// message: 'Yukingiz uchun yetkazib beruvchi tayinlandi: Azamat A.',
// },
// {
// id: 6,
// title: 'Yuk omborga yetib keldi',
// message: 'Yuk 98765 muvaffaqiyatli ravishda omborga yetib keldi.',
// },
];

View File

@@ -0,0 +1,121 @@
import NavbarBack from 'components/NavbarBack';
import Navigation from 'components/Navigation';
import NoResult from 'components/NoResult';
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import {
RefreshControl,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import Clock from 'svg/Clock';
import { fakeNotifications, NotificationsData } from '../lib/data';
import NotificationsModal from './NotificationsModal';
interface NotificationsProps {}
const Notifications = (props: NotificationsProps) => {
const [refreshing, setRefreshing] = React.useState(false);
const [modalVisible, setModalVisible] = React.useState(false);
const { t } = useTranslation();
const [selectedOrder, setSelectedOrder] =
React.useState<NotificationsData | null>(null);
const onRefresh = React.useCallback(() => {
setRefreshing(true);
setTimeout(() => {
setRefreshing(false);
}, 1500);
}, []);
const openModal = React.useCallback((item: NotificationsData) => {
setSelectedOrder(item);
setModalVisible(true);
}, []);
const refreshControl = React.useMemo(
() => <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />,
[refreshing, onRefresh],
);
if (!(fakeNotifications.length > 0)) {
return (
<SafeAreaView style={{ flex: 1 }}>
<NavbarBack title={t('Bildirishnomalar')} />
<View style={styles.container}>
<NoResult />
</View>
<Navigation />
</SafeAreaView>
);
}
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={styles.container}>
<NavbarBack title={t('Bildirishnomalar')} />
<ScrollView
keyboardShouldPersistTaps="handled"
refreshControl={refreshControl}
>
{fakeNotifications.map(item => (
<TouchableOpacity
onPress={() => openModal(item)}
style={styles.card}
key={item.id}
>
<Text style={styles.text}>{item.message}</Text>
<Clock color="#000000" />
</TouchableOpacity>
))}
</ScrollView>
<NotificationsModal
visible={modalVisible}
setVisible={setModalVisible}
selectedOrder={selectedOrder}
/>
<Navigation />
</View>
</SafeAreaView>
);
};
export default Notifications;
const styles = StyleSheet.create({
container: {
flex: 1,
},
header: {
paddingHorizontal: 20,
paddingTop: 5,
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#333',
},
card: {
borderBottomWidth: 1,
borderColor: '#D8DADC',
width: '95%',
marginLeft: 'auto',
marginRight: 'auto',
marginTop: 20,
paddingBottom: 18,
paddingLeft: 12,
paddingRight: 12,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
text: {
fontSize: 18,
fontWeight: '400',
width: '80%',
},
});

View File

@@ -0,0 +1,186 @@
import React, { useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import {
Animated,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import CloseIcon from 'svg/Close';
import { NotificationsData } from '../lib/data';
interface Props {
visible: boolean;
setVisible: (val: boolean) => void;
selectedOrder: NotificationsData | null;
}
const NotificationsModal = ({ visible, setVisible, selectedOrder }: Props) => {
const opacity = useRef(new Animated.Value(0)).current;
const translateY = useRef(new Animated.Value(50)).current;
const { t } = useTranslation();
const closeModal = () => {
Animated.timing(opacity, {
toValue: 0,
duration: 200,
useNativeDriver: true,
}).start(() => setVisible(false));
};
useEffect(() => {
if (visible) {
Animated.parallel([
Animated.timing(opacity, {
toValue: 1,
duration: 250,
useNativeDriver: true,
}),
Animated.spring(translateY, {
toValue: 0,
useNativeDriver: true,
}),
]).start();
}
}, [visible]);
if (!visible || !selectedOrder) return null;
return (
<View style={styles.overlay}>
<Animated.View
style={[
styles.modalContent,
{
opacity: opacity,
transform: [{ translateY }],
},
]}
>
<View style={styles.header}>
<Text style={styles.title}>{selectedOrder.title}</Text>
<TouchableOpacity onPress={closeModal} style={styles.closeBtn}>
<CloseIcon width={15} height={15} color={'#000'} />
</TouchableOpacity>
</View>
<View style={styles.divider} />
<Text style={styles.sectionTitle}>{selectedOrder.message}</Text>
<TouchableOpacity onPress={closeModal} style={styles.btn}>
<Text style={styles.btnText}>{t('Yopish')}</Text>
</TouchableOpacity>
</Animated.View>
</View>
);
};
const styles = StyleSheet.create({
overlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0,0,0,0.4)',
justifyContent: 'center',
alignItems: 'center',
zIndex: 100,
},
modalContent: {
width: '90%',
backgroundColor: '#fff',
borderRadius: 10,
paddingBottom: 20,
shadowColor: '#000',
shadowOpacity: 0.3,
shadowRadius: 4,
elevation: 6,
},
header: {
padding: 20,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
title: {
fontSize: 18,
fontWeight: '600',
},
closeBtn: {
padding: 5,
width: 30,
height: 30,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#0000000D',
borderRadius: 50,
},
divider: {
height: 1,
backgroundColor: '#E0E0E0',
marginBottom: 10,
},
sectionTitle: {
fontSize: 16,
fontWeight: '600',
paddingHorizontal: 20,
marginBottom: 8,
},
productItem: {
paddingHorizontal: 20,
paddingBottom: 12,
borderBottomWidth: 1,
borderBottomColor: '#eee',
marginBottom: 10,
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
},
productName: {
fontWeight: '500',
fontSize: 15,
},
productTrekId: {
color: '#555',
},
detail: {
color: '#555',
marginTop: 8,
},
totalRow: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingHorizontal: 20,
paddingTop: 10,
borderTopWidth: 1,
borderTopColor: '#eee',
},
totalLabel: {
fontSize: 16,
fontWeight: '600',
},
totalValue: {
fontSize: 16,
fontWeight: '600',
color: '#28A7E8',
},
btn: {
alignSelf: 'center',
marginTop: 20,
height: 48,
width: '90%',
borderRadius: 8,
justifyContent: 'center',
backgroundColor: '#28A7E8',
},
btnText: {
textAlign: 'center',
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
});
export default NotificationsModal;

View File

@@ -0,0 +1,442 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useNavigation, useRoute } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import React, { useEffect, useRef, useState } from 'react';
import {
Alert,
Dimensions,
PanResponder,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
UIManager,
View,
findNodeHandle,
} from 'react-native';
import { CodeField, Cursor } from 'react-native-confirmation-code-field';
import { SafeAreaView } from 'react-native-safe-area-context';
import Svg, { Line } from 'react-native-svg';
const LOCK_ENABLED = 'LOCK_ENABLED';
const LOCK_TYPE = 'LOCK_TYPE';
const LOCK_PASSWORD = 'LOCK_PASSWORD';
const LOCK_PATTERN = 'LOCK_PATTERN';
type LockType = 'pin' | 'password' | 'pattern';
type PatternLockProps = {
path: number[];
setPath: React.Dispatch<React.SetStateAction<number[]>>;
onPatternComplete: (pattern: string | null) => void;
patternError?: boolean;
};
const PatternLock = ({
path,
setPath,
onPatternComplete,
patternError,
}: PatternLockProps) => {
const { width } = Dimensions.get('window');
const size = width * 0.8;
const cellSize = size / 3;
const containerRef = useRef<View>(null);
const [layout, setLayout] = useState({ pageX: 0, pageY: 0 });
const [currentPos, setCurrentPos] = useState<{ x: number; y: number } | null>(
null,
);
const points = Array.from({ length: 9 }, (_, i) => ({
id: i + 1,
x: (i % 3) * cellSize + cellSize / 2,
y: Math.floor(i / 3) * cellSize + cellSize / 2,
}));
const panResponder = PanResponder.create({
onStartShouldSetPanResponder: () => true,
onMoveShouldSetPanResponder: () => true,
onPanResponderGrant: () => {
setPath([]); // yangi chizma
setCurrentPos(null);
},
onPanResponderMove: (_e, gestureState) => {
const x = gestureState.moveX - layout.pageX;
const y = gestureState.moveY - layout.pageY;
setCurrentPos({ x, y });
const point = points.find(
p =>
Math.abs(p.x - x) < cellSize / 3 && Math.abs(p.y - y) < cellSize / 3,
);
if (point && !path.includes(point.id)) {
setPath(prev => {
const newPath = [...prev, point.id];
return newPath;
});
}
},
onPanResponderRelease: () => {
setCurrentPos(null);
onPatternComplete(path.length > 0 ? path.join('') : null);
},
});
useEffect(() => {
if (containerRef.current) {
const handle = findNodeHandle(containerRef.current);
if (handle) {
UIManager.measure(handle, (_x, _y, _w, _h, pageX, pageY) => {
setLayout({ pageX, pageY });
});
}
}
}, []);
return (
<View
ref={containerRef}
style={[styles.patternContainer, { width: size, height: size }]}
{...panResponder.panHandlers}
>
<Svg style={StyleSheet.absoluteFill}>
{path.map((id, idx) => {
if (idx === 0) return null;
const from = points.find(p => p.id === path[idx - 1]);
const to = points.find(p => p.id === id);
if (!from || !to) return null;
return (
<Line
key={`${from.id}-${to.id}`}
x1={from.x}
y1={from.y}
x2={to.x}
y2={to.y}
stroke="#28A7E8"
strokeWidth={4}
/>
);
})}
{currentPos && path.length > 0 && (
<Line
x1={points.find(p => p.id === path[path.length - 1])?.x}
y1={points.find(p => p.id === path[path.length - 1])?.y}
x2={currentPos.x}
y2={currentPos.y}
stroke="#28A7E8"
strokeWidth={2}
strokeDasharray="4"
/>
)}
</Svg>
{points.map(point => (
<View
key={point.id}
style={[
styles.patternPoint,
path.includes(point.id)
? patternError
? styles.patternPointError
: styles.patternPointActive
: {},
{ left: point.x - 15, top: point.y - 15 },
]}
/>
))}
</View>
);
};
const AddedLock = () => {
const [step, setStep] = useState<1 | 2>(1);
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
// Pattern states
const [patternPath, setPatternPath] = useState<number[]>([]);
const [patternTemp, setPatternTemp] = useState<string | null>(null);
const [patternConfirmed, setPatternConfirmed] = useState(false);
const [patternError, setPatternError] = useState(false);
const route = useRoute<any>();
const navigation = useNavigation<NativeStackNavigationProp<any>>();
const paramLockType = route.params?.lockType;
const validLockTypes: LockType[] = ['pin', 'password', 'pattern'];
const initialLockType: LockType = validLockTypes.includes(paramLockType)
? paramLockType
: 'pin';
const [lockType] = useState<LockType>(initialLockType);
const onContinue = async () => {
if (step === 1) {
if (lockType === 'pattern') {
if (!patternTemp) {
return Alert.alert('Error', 'Please draw your pattern first');
}
setPatternPath([]); // clear for second step
} else {
if (!password) {
return Alert.alert('Error', 'Please enter your PIN/Password');
}
}
setStep(2);
return;
}
if (lockType === 'pattern') {
if (!patternConfirmed) {
return Alert.alert('Error', 'Please confirm your pattern');
}
await AsyncStorage.setItem(LOCK_PATTERN, patternTemp!);
} else {
if (!confirmPassword) {
return Alert.alert('Error', 'Please confirm your PIN/Password');
}
if (password !== confirmPassword) {
return Alert.alert('Error', 'Values do not match');
}
await AsyncStorage.setItem(LOCK_PASSWORD, password);
}
await AsyncStorage.setItem(LOCK_ENABLED, 'true');
await AsyncStorage.setItem(LOCK_TYPE, lockType);
Alert.alert('Success', 'Lock settings saved');
resetForm();
navigation.goBack();
};
const resetForm = () => {
setStep(1);
setPassword('');
setConfirmPassword('');
setPatternPath([]);
setPatternTemp(null);
setPatternConfirmed(false);
};
const handlePatternComplete = (inputPattern: string | null) => {
if (!inputPattern || inputPattern.length < 4) {
setPatternError(true);
setTimeout(() => {
setPatternError(false);
setPatternPath([]);
}, 800);
return;
}
if (step === 1) {
setPatternTemp(inputPattern);
} else {
if (patternTemp && inputPattern === patternTemp) {
setPatternConfirmed(true);
Alert.alert('Success', 'Pattern confirmed');
} else {
setPatternError(true);
setTimeout(() => {
setPatternError(false);
setStep(1);
setPatternPath([]);
}, 800);
}
}
};
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={styles.container}>
<View style={styles.lockCard}>
{lockType === 'pattern' ? (
<View style={{ alignItems: 'center' }}>
{step === 1 ? (
<>
<Text style={{ textAlign: 'center', marginBottom: 10 }}>
Draw your pattern
</Text>
<PatternLock
path={patternPath}
setPath={setPatternPath}
onPatternComplete={handlePatternComplete}
patternError={patternError}
/>
</>
) : (
<>
<Text style={{ textAlign: 'center', marginBottom: 10 }}>
Confirm your pattern
</Text>
<PatternLock
path={patternPath}
setPath={setPatternPath}
onPatternComplete={handlePatternComplete}
/>
</>
)}
</View>
) : lockType === 'pin' ? (
<>
{step === 1 ? (
<>
<Text style={{ textAlign: 'center', marginBottom: 10 }}>
Enter PIN
</Text>
<CodeField
value={password}
onChangeText={setPassword}
cellCount={4}
rootStyle={{ marginBottom: 20 }}
keyboardType="number-pad"
renderCell={({ index, symbol, isFocused }) => (
<View
key={index}
style={[
styles.pinCell,
isFocused && styles.pinCellFocused,
]}
>
<Text style={{ fontSize: 24 }}>
{symbol || (isFocused ? <Cursor /> : null)}
</Text>
</View>
)}
/>
</>
) : (
<>
<Text style={{ textAlign: 'center', marginBottom: 10 }}>
Confirm PIN
</Text>
<CodeField
value={confirmPassword}
onChangeText={setConfirmPassword}
cellCount={4}
rootStyle={{ marginBottom: 20 }}
keyboardType="number-pad"
renderCell={({ index, symbol, isFocused }) => (
<View
key={index}
style={[
styles.pinCell,
isFocused && styles.pinCellFocused,
]}
>
<Text style={{ fontSize: 24 }}>
{symbol || (isFocused ? <Cursor /> : null)}
</Text>
</View>
)}
/>
</>
)}
</>
) : (
<>
{step === 1 ? (
<TextInput
placeholder="Enter Password"
secureTextEntry
style={styles.input}
value={password}
onChangeText={setPassword}
/>
) : (
<TextInput
placeholder="Confirm Password"
secureTextEntry
style={styles.input}
value={confirmPassword}
onChangeText={setConfirmPassword}
/>
)}
</>
)}
</View>
<View style={styles.btnCard}>
<TouchableOpacity
style={[styles.button, styles.removeButton]}
onPress={() => {
resetForm();
navigation.goBack();
}}
>
<Text style={styles.buttonText}>Bekor qilish</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.button} onPress={onContinue}>
<Text style={styles.buttonText}>
{step === 1 ? 'Davom etish' : 'Saqlash'}
</Text>
</TouchableOpacity>
</View>
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: { flex: 1 },
btnCard: {
position: 'absolute',
flexDirection: 'row',
bottom: 5,
width: '100%',
justifyContent: 'center',
gap: 10,
},
patternContainer: {
borderRadius: 10,
justifyContent: 'center',
alignItems: 'center',
},
patternPoint: {
width: 30,
height: 30,
borderRadius: 15,
backgroundColor: '#ddd',
position: 'absolute',
},
patternPointActive: { backgroundColor: '#28A7E8' },
pinCell: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
width: 50,
height: 50,
justifyContent: 'center',
alignItems: 'center',
margin: 5,
},
pinCellFocused: { borderColor: '#28A7E8' },
input: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
padding: 15,
marginBottom: 15,
fontSize: 16,
},
button: {
backgroundColor: '#28A7E8',
padding: 15,
borderRadius: 8,
alignItems: 'center',
width: '48%',
},
removeButton: { backgroundColor: '#ccc' },
buttonText: { color: '#fff', fontSize: 16, fontWeight: 'bold' },
lockCard: {
height: '100%',
justifyContent: 'center',
width: '95%',
margin: 'auto',
},
patternPointError: { backgroundColor: 'red' },
});
export default AddedLock;

View File

@@ -0,0 +1,181 @@
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import NavbarBack from 'components/NavbarBack';
import Navigation from 'components/Navigation';
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import {
Image,
RefreshControl,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
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 Check from 'svg/Check';
import { changeLanguage } from 'utils/changeLanguage';
interface SettingsProps {}
const languages = [
{ code: 'uz', label: "O'zbek tili", Icon: UZ },
{ code: 'ru', label: 'Rus tili', Icon: RU },
];
const Settings = (props: SettingsProps) => {
const [refreshing, setRefreshing] = React.useState(false);
const { i18n, t } = useTranslation();
const navigation = useNavigation<NativeStackNavigationProp<any>>();
const selectedLang = languages.find(l => l.code === i18n.language);
const handlechangeLanguage = (lang: string) => {
changeLanguage(lang);
};
const onRefresh = React.useCallback(() => {
setRefreshing(true);
setTimeout(() => {
setRefreshing(false);
}, 1500);
}, []);
const refreshControl = React.useMemo(
() => <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />,
[refreshing, onRefresh],
);
const contentContainerStyle = React.useMemo(
() => ({ paddingBottom: 10 }),
[],
);
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={styles.container}>
<NavbarBack title={t('Sozlamalar')} />
<ScrollView
keyboardShouldPersistTaps="handled"
refreshControl={refreshControl}
contentContainerStyle={contentContainerStyle}
>
<View style={{ marginTop: 10, width: '95%', margin: 'auto' }}>
<Text style={{ fontSize: 20, fontWeight: '500' }}>
{t('select_language')}
</Text>
{languages.map(item => (
<TouchableOpacity
style={[
styles.card,
{
backgroundColor:
selectedLang?.code === item.code
? '#28A7E81A'
: '#FFFFFF',
shadowColor:
selectedLang?.code === item.code
? '#28A7E81A'
: '#F3FAFF',
},
]}
key={item.code}
onPress={() => handlechangeLanguage(item.code)}
>
<View
style={{
flexDirection: 'row',
gap: 10,
alignItems: 'center',
}}
>
<Image
source={item.Icon}
style={{ width: 30, height: 30, objectFit: 'contain' }}
/>
<Text
style={[
styles.label,
{
color:
selectedLang?.code === item.code
? '#28A7E8'
: '#000000',
},
]}
>
{item.label}
</Text>
</View>
<View
style={[
styles.check,
{
backgroundColor:
selectedLang?.code === item.code
? '#28A7E8'
: '#FFFFFF',
borderColor:
selectedLang?.code === item.code
? '#28A7E8'
: '#383838',
},
]}
>
{selectedLang?.code === item.code && (
<Check width={20} height={20} color="#ffff" />
)}
</View>
</TouchableOpacity>
))}
</View>
{/* <TouchableOpacity
style={[styles.card, { width: '95%', margin: 'auto' }]}
onPress={() => navigation.navigate('SettingsLock')}
>
<Text>Parol o'rnatish</Text>
<ArrowRightUnderline width={20} height={20} color="#383838" />
</TouchableOpacity> */}
</ScrollView>
<Navigation />
</View>
</SafeAreaView>
);
};
export default Settings;
const styles = StyleSheet.create({
container: {
flex: 1,
},
card: {
backgroundColor: '#FFFFFF',
shadowColor: '#F3FAFF',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 4,
flexDirection: 'row',
justifyContent: 'space-between',
padding: 15,
marginTop: 10,
borderRadius: 10,
alignItems: 'center',
},
label: {
fontWeight: '400',
fontSize: 16,
},
check: {
width: 20,
height: 20,
borderWidth: 1,
borderColor: '#383838',
borderRadius: 6,
alignItems: 'center',
justifyContent: 'center',
},
});

View File

@@ -0,0 +1,86 @@
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import NavbarBack from 'components/NavbarBack';
import {
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import ArrowRightUnderline from 'svg/ArrowRightUnderline';
const SettingsLock = () => {
const navigation = useNavigation<NativeStackNavigationProp<any>>();
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={styles.container}>
<NavbarBack title="Parol o'rnatish" />
<ScrollView>
<View style={{ width: '95%', margin: 'auto', marginTop: 20 }}>
<Text style={styles.title}>Parol turini tanlang</Text>
<TouchableOpacity
style={[styles.card]}
onPress={() =>
navigation.navigate('AddLock', { lockType: 'pin' })
}
>
<Text style={styles.label}>Pin-kod</Text>
<ArrowRightUnderline color="#383838" />
</TouchableOpacity>
<TouchableOpacity
style={[styles.card]}
onPress={() =>
navigation.navigate('AddLock', { lockType: 'password' })
}
>
<Text style={styles.label}>Parol</Text>
<ArrowRightUnderline color="#383838" />
</TouchableOpacity>
<TouchableOpacity
style={[styles.card]}
onPress={() =>
navigation.navigate('AddLock', { lockType: 'pattern' })
}
>
<Text style={styles.label}>Chizma</Text>
<ArrowRightUnderline color="#383838" />
</TouchableOpacity>
</View>
</ScrollView>
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
title: {
fontSize: 16,
fontWeight: '500',
},
label: {
fontWeight: '400',
fontSize: 16,
},
card: {
backgroundColor: '#FFFFFF',
shadowColor: '#F3FAFF',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 4,
flexDirection: 'row',
justifyContent: 'space-between',
padding: 15,
marginTop: 10,
borderRadius: 10,
alignItems: 'center',
},
});
export default SettingsLock;

View File

@@ -0,0 +1,181 @@
import NavbarBack from 'components/NavbarBack';
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import {
KeyboardAvoidingView,
Platform,
RefreshControl,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
interface SupportProps {}
interface Message {
id: number;
text: string;
fromUser: boolean;
}
const fakeMessages: Message[] = [
{ id: 1, text: 'Salom! Yukim qayerda?', fromUser: true },
{
id: 2,
text: 'Salom! Yukingiz hozirda Toshkent omborida.',
fromUser: false,
},
{ id: 3, text: 'Yetib kelish vaqti qachon?', fromUser: true },
{
id: 4,
text: 'Taxminan ertaga soat 15:00 gacha yetkaziladi.',
fromUser: false,
},
];
const Support = (props: SupportProps) => {
const [refreshing, setRefreshing] = React.useState(false);
const [messages, setMessages] = React.useState<Message[]>(fakeMessages);
const [input, setInput] = React.useState('');
const { t } = useTranslation();
const onRefresh = React.useCallback(() => {
setRefreshing(true);
setTimeout(() => {
setRefreshing(false);
}, 1500);
}, []);
const sendMessage = () => {
if (input.trim() === '') return;
const newMessage: Message = {
id: Date.now(),
text: input,
fromUser: true,
};
setMessages(prev => [...prev, newMessage]);
setInput('');
};
return (
<SafeAreaView style={{ flex: 1, backgroundColor: '#fff' }}>
<View style={styles.container}>
<NavbarBack title={t('Yordam xizmati')} />
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
keyboardVerticalOffset={80}
>
<ScrollView
keyboardShouldPersistTaps="handled"
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
contentContainerStyle={styles.chatContainer}
>
{messages.map(msg => (
<View
key={msg.id}
style={[
styles.messageBubble,
msg.fromUser ? styles.userBubble : styles.supportBubble,
]}
>
<Text
style={msg.fromUser ? styles.userText : styles.supportText}
>
{msg.text}
</Text>
</View>
))}
</ScrollView>
<View style={styles.inputContainer}>
<TextInput
placeholder={t('Xabar yozing...')}
value={input}
onChangeText={setInput}
style={styles.input}
placeholderTextColor="#999"
/>
<TouchableOpacity onPress={sendMessage} style={styles.sendButton}>
<Text style={styles.sendButtonText}>{t('Yuborish')}</Text>
</TouchableOpacity>
</View>
</KeyboardAvoidingView>
</View>
</SafeAreaView>
);
};
export default Support;
const styles = StyleSheet.create({
container: {
flex: 1,
},
chatContainer: {
padding: 16,
paddingBottom: 80,
},
messageBubble: {
maxWidth: '75%',
padding: 12,
borderRadius: 16,
marginBottom: 10,
},
userBubble: {
backgroundColor: '#007AFF',
alignSelf: 'flex-end',
borderTopRightRadius: 0,
},
supportBubble: {
backgroundColor: '#F1F1F1',
alignSelf: 'flex-start',
borderTopLeftRadius: 0,
},
userText: {
color: '#fff',
fontSize: 16,
},
supportText: {
color: '#000',
fontSize: 16,
},
inputContainer: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
backgroundColor: '#fff',
paddingHorizontal: 12,
paddingVertical: 8,
flexDirection: 'row',
alignItems: 'center',
borderTopWidth: 1,
borderColor: '#E5E5E5',
},
input: {
flex: 1,
height: 40,
backgroundColor: '#F5F5F5',
borderRadius: 20,
paddingHorizontal: 16,
fontSize: 16,
marginRight: 8,
},
sendButton: {
backgroundColor: '#007AFF',
borderRadius: 20,
paddingVertical: 8,
paddingHorizontal: 16,
},
sendButtonText: {
color: '#fff',
fontSize: 14,
fontWeight: '600',
},
});

View File

@@ -0,0 +1,179 @@
import Clipboard from '@react-native-clipboard/clipboard';
import { useQuery } from '@tanstack/react-query';
import { authApi } from 'api/auth';
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
Alert,
FlatList,
StyleSheet,
Text,
TouchableOpacity,
useWindowDimensions,
View,
} from 'react-native';
import Toast from 'react-native-toast-message';
import AntDesign from 'react-native-vector-icons/AntDesign';
import Copy from 'svg/Copy';
import Kitay from 'svg/Ki';
const TabsAutoWarehouses = () => {
const { width: screenWidth } = useWindowDimensions();
const scale = screenWidth < 360 ? 0.85 : 1;
const cardWidth = screenWidth * 0.95;
const { t } = useTranslation();
const styles = makeStyles(scale, cardWidth, screenWidth);
const { data: getMe } = useQuery({
queryKey: ['getMe'],
queryFn: authApi.getMe,
});
const addressInfo = [
{
id: 1,
title: 'China (Auto)',
postCode: '510440',
addressInfo: [
`收货人: ${getMe?.aviaCargoId}`,
'手机号码: 18335530701',
'北京市顺义区南法信旭辉空港中心C座',
`1004 ${getMe?.aviaCargoId}`,
],
},
// {
// id: 2,
// title: 'Korea (Auto)',
// postCode: '520550',
// addressInfo: [
// '收件人李小龙AT(M312)',
// '地址∶深圳市南山区科技园科发路',
// '18号AT(M312)',
// '电话: 13800008888',
// ],
// },
];
const handleCopy = (info: string[]) => {
if (getMe?.status === 'active') {
const textToCopy = info.join('\n');
Clipboard.setString(textToCopy);
Toast.show({
type: 'success',
text1: t('Nusxa olingan'),
text2: t('Avto manzili nusxalandi!'),
position: 'top',
visibilityTime: 2000,
});
} else {
Toast.show({
type: 'error',
text1: t('Xatolik yuz berdi!'),
text2: t('Akkaunt faol emas!'),
position: 'top',
visibilityTime: 2000,
});
}
};
return (
<FlatList
data={addressInfo}
horizontal
keyExtractor={item => item.id.toString()}
pagingEnabled
showsHorizontalScrollIndicator={false}
snapToInterval={cardWidth + 10}
decelerationRate="fast"
renderItem={({ item, index }) => {
const isLast = index === addressInfo.length - 1;
return (
<View style={[styles.card, { marginRight: isLast ? 0 : 10 }]}>
<View style={styles.titleCard}>
<Kitay width={24 * scale} height={24 * scale} />
<Text style={styles.title}>{item.title}</Text>
</View>
<View style={styles.infoId}>
<View style={{ gap: 4 * scale }}>
{item.addressInfo.map((line, idx) => (
<Text key={idx} style={styles.infoText}>
{line}
</Text>
))}
</View>
<TouchableOpacity onPress={() => handleCopy(item.addressInfo)}>
<Copy color="#28A7E8" width={24 * scale} height={24 * scale} />
</TouchableOpacity>
</View>
<View style={styles.postCodeWrapper}>
<Text style={styles.postCodeText}>{t('Auto post kodi')}: </Text>
<Text style={styles.postCode}>{item.postCode}</Text>
<TouchableOpacity
onPress={() => {
Clipboard.setString(item.postCode);
Alert.alert(t('Nusxa olindi'), t('Pochta kodi nusxalandi!'));
}}
style={{ marginLeft: 4 * scale }}
>
<AntDesign
name="pushpin"
color="red"
size={16 * scale}
style={{ transform: [{ rotateY: '180deg' }] }}
/>
</TouchableOpacity>
</View>
</View>
);
}}
/>
);
};
const makeStyles = (scale: number, cardWidth: number, screenWidth: number) =>
StyleSheet.create({
card: {
height: 220 * scale,
width: cardWidth,
backgroundColor: '#28a8e82c',
borderRadius: 12 * scale,
padding: 15 * scale,
gap: 10 * scale,
},
titleCard: {
flexDirection: 'row',
gap: 8 * scale,
alignItems: 'center',
},
title: {
fontSize: 20 * scale,
fontWeight: '600',
color: '#101623CC',
},
infoId: {
flexDirection: 'row',
justifyContent: 'space-between',
marginVertical: 8 * scale,
},
infoText: {
fontSize: 16 * scale,
color: '#28A7E8',
},
postCodeWrapper: {
flexDirection: 'row',
alignItems: 'center',
},
postCodeText: {
fontSize: 16 * scale,
color: '#000000',
fontWeight: '500',
},
postCode: {
fontSize: 16 * scale,
color: '#28A7E8',
fontWeight: '400',
marginLeft: 4 * scale,
},
});
export default TabsAutoWarehouses;

View File

@@ -0,0 +1,191 @@
import Clipboard from '@react-native-clipboard/clipboard';
import { useQuery } from '@tanstack/react-query';
import { authApi } from 'api/auth';
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
Alert,
FlatList,
StyleSheet,
Text,
TouchableOpacity,
useWindowDimensions,
View,
} from 'react-native';
import Toast from 'react-native-toast-message';
import AntDesign from 'react-native-vector-icons/AntDesign';
import Copy from 'svg/Copy';
import Kitay from 'svg/Ki';
const TabsAviaWarehouses = () => {
const { data: getMe, isLoading: getMeLoad } = useQuery({
queryKey: ['getMe'],
queryFn: authApi.getMe,
});
const addressList = [
{
id: 1,
title: 'China (Avia)',
postCode: '510440',
addressInfo: [
`收货人: ${getMe?.aviaCargoId}`,
'手机号码: 18335530701',
'北京市顺义区南法信旭辉空港中心C座',
`1004 ${getMe?.aviaCargoId}`,
],
},
// {
// id: 2,
// title: 'Korea (Avia)',
// postCode: '510440',
// addressInfo: [
// '收货人: M312',
// '手机号码: 18335530701',
// '北京市顺义区南法信旭辉空港中心C座',
// '1004 N209',
// ],
// },
];
const { width: screenWidth } = useWindowDimensions();
const scale = screenWidth < 360 ? 0.85 : 1;
const cardWidth = screenWidth * 0.95;
const styles = makeStyles(scale, cardWidth, screenWidth);
const { t } = useTranslation();
const handleCopy = (info: string[]) => {
if (getMe?.status === 'active') {
const textToCopy = info.join('\n');
Clipboard.setString(textToCopy);
Toast.show({
type: 'success',
text1: t('Nusxa olingan'),
text2: t('Avia manzili nusxalandi!'),
position: 'top',
visibilityTime: 2000,
});
} else {
Toast.show({
type: 'error',
text1: t('Xatolik yuz berdi!'),
text2: t('Akkaunt faol emas!'),
position: 'top',
visibilityTime: 2000,
});
}
};
return (
<FlatList
data={addressList}
horizontal
keyExtractor={item => item.id.toString()}
pagingEnabled
showsHorizontalScrollIndicator={false}
snapToInterval={cardWidth + 10} // +10: marginRight
decelerationRate="fast"
renderItem={({ item, index }) => {
const isLast = index === addressList.length - 1;
return (
<View style={[styles.card, { marginRight: isLast ? 0 : 10 }]}>
<View style={styles.titleCard}>
<Kitay width={24 * scale} height={24 * scale} />
<Text style={styles.title}>{item.title}</Text>
</View>
<View style={styles.infoId}>
<View style={{ gap: 4 * scale }}>
{item.addressInfo.map((line, idx) => (
<Text key={idx} style={styles.infoText}>
{line}
</Text>
))}
</View>
<TouchableOpacity onPress={() => handleCopy(item.addressInfo)}>
<Copy color="#28A7E8" width={24 * scale} height={24 * scale} />
</TouchableOpacity>
</View>
<View style={styles.postCodeWrapper}>
<Text style={styles.postCodeText}>{t('Avia post kodi')}: </Text>
<Text style={styles.postCode}>{item.postCode}</Text>
<TouchableOpacity
onPress={() => {
Clipboard.setString(item.postCode);
Alert.alert(t('Nusxa olindi'), t('Pochta kodi nusxalandi!'));
}}
style={{ marginLeft: 4 * scale }}
>
<AntDesign
name="pushpin"
color="red"
size={16 * scale}
style={{ transform: [{ rotateY: '180deg' }] }}
/>
</TouchableOpacity>
</View>
</View>
);
}}
/>
);
};
const makeStyles = (scale: number, cardWidth: number, screenWidth: number) =>
StyleSheet.create({
container: {
height: 200,
width: '95%',
backgroundColor: '#28a8e82c',
margin: 'auto',
marginTop: 20,
borderRadius: 12,
padding: 12,
gap: 10,
},
scrollContainer: {
marginTop: 20,
paddingHorizontal: (screenWidth - cardWidth) / 2,
},
postCodeWrapper: {
flexDirection: 'row',
alignItems: 'center',
},
card: {
height: 220 * scale,
width: cardWidth,
backgroundColor: '#28a8e82c',
borderRadius: 12 * scale,
padding: 15 * scale,
gap: 10 * scale,
},
titleCard: {
flexDirection: 'row',
gap: 8,
alignItems: 'center',
},
title: {
fontSize: 20,
fontWeight: '600',
color: '#101623CC',
},
infoId: {
flexDirection: 'row',
justifyContent: 'space-between',
},
infoText: {
fontSize: 16,
color: '#28A7E8',
},
postCodeText: {
fontSize: 16,
color: '#000000',
fontWeight: '500',
},
postCode: {
fontSize: 16,
color: '#28A7E8',
fontWeight: '400',
},
});
export default TabsAviaWarehouses;

View File

@@ -0,0 +1,123 @@
import SingleFileDrop from 'components/FileDrop';
import NavbarBack from 'components/NavbarBack';
import Navigation from 'components/Navigation';
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import {
RefreshControl,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import TabsAutoWarehouses from './TabsAutoWarehouses';
import TabsAviaWarehouses from './TabsAviaWarehouses';
interface WarehousesProps {}
const Warehouses = (props: WarehousesProps) => {
const [refreshing, setRefreshing] = React.useState(false);
const { t } = useTranslation();
const onRefresh = React.useCallback(() => {
setRefreshing(true);
setTimeout(() => {
setRefreshing(false);
}, 1500);
}, []);
const refreshControl = React.useMemo(
() => <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />,
[refreshing, onRefresh],
);
const contentContainerStyle = React.useMemo(
() => ({ paddingBottom: 10 }),
[],
);
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={styles.container}>
<NavbarBack title={t('Xitoy omborlari manzili')} />
<ScrollView
keyboardShouldPersistTaps="handled"
refreshControl={refreshControl}
contentContainerStyle={contentContainerStyle}
>
<View style={styles.card}>
<Text style={styles.title}>{t('Bizning Xitoy manzilimiz')}</Text>
<Text style={styles.text}>
{t(
'Taobao, pinduoduo, 1688 ,alibaba va Xitoyning istalgan platformasiga kiritish uchun',
)}
</Text>
<TabsAviaWarehouses />
<TabsAutoWarehouses />
<Text style={styles.title}>
{t('Xitoy omborlarimiz manzilini programmaga kiriting')}
</Text>
<View style={{ gap: 20 }}>
<Text style={styles.text}>
{t(
"Diqqat! Iltimos, Xitoy omborimiz manzilini Xitoy programmalariga kiritganingizdan so'ng, kiritilgan holatdagi skrenshotni bizga yuborib, tekshirtiring",
)}
</Text>
<Text style={styles.text}>
{t(
"Xitoy ombori manzilini to'g'ri kiritish, mahsulotingiz yo'qolib qolish oldini oladi.",
)}
</Text>
<Text style={styles.text}>
{t(
"Agar sizda savol tug'ilsa yoki biron narsaga tushunmasangiz bizga murojaat qiling",
)}
</Text>
</View>
<Text style={styles.title}>{t('Skrenshot rasmini yuklang')}</Text>
<SingleFileDrop title={t('Rasmni shu yerga yuklang')} />
<TouchableOpacity style={styles.button}>
<Text style={styles.btnText}>{t('Manzilni tekshirish')}</Text>
</TouchableOpacity>
</View>
</ScrollView>
<Navigation />
</View>
</SafeAreaView>
);
};
export default Warehouses;
const styles = StyleSheet.create({
container: { flex: 1 },
card: {
width: '95%',
marginTop: 20,
margin: 'auto',
gap: 10,
},
title: {
fontSize: 20,
fontWeight: '500',
},
text: {
fontSize: 18,
color: '#000000B2',
fontWeight: '400',
},
button: {
backgroundColor: '#28A7E8',
height: 56,
borderRadius: 8,
textAlign: 'center',
display: 'flex',
justifyContent: 'center',
},
btnText: {
color: '#fff',
fontSize: 20,
textAlign: 'center',
},
});

View File

@@ -0,0 +1,474 @@
export interface ProductItem {
trekId: string;
name: string;
weight: string;
quantity: number;
totalPrice: string;
}
export interface DataInfo {
id: number;
packetName: string;
weight: number;
totalPrice: number;
paymentStatus: string;
deliveryStatus: string;
paymentType: string;
items: {
trekId: string;
name: string;
weight: number;
price: number;
totalPrice: number;
}[];
qrCode: string;
}
export interface PacketItem {
id: number;
packetName: string;
weight: number;
totalPrice: number;
paymentStatus: string;
paymentType: string;
deliveryStatus: string;
items: {
trekId: string;
name: string;
weight: number;
price: number;
totalPrice: number;
}[];
qrCode: string;
}
export interface PacketsDataFilter {
data: PacketItem[];
totalPages: number;
totalElements: number;
}
// export const fakeDataOrder: DataInfo[] = [
// {
// status: 'gathering',
// reys: 'REYS-A1',
// weight: '18kg',
// price: '118 470 som',
// cargo: 'auto',
// products: [
// {
// trekId: 'TRK001',
// name: 'Avto detal A1',
// weight: '6kg',
// quantity: 2,
// totalPrice: '78 980 som',
// },
// {
// trekId: 'TRK002',
// name: 'Filtr A2',
// weight: '3kg',
// quantity: 3,
// totalPrice: '39 490 som',
// },
// {
// trekId: 'TRK002',
// name: 'Filtr A2',
// weight: '3kg',
// quantity: 3,
// totalPrice: '39 490 som',
// },
// {
// trekId: 'TRK002',
// name: 'Filtr A2',
// weight: '3kg',
// quantity: 3,
// totalPrice: '39 490 som',
// },
// {
// trekId: 'TRK002',
// name: 'Filtr A2',
// weight: '3kg',
// quantity: 3,
// totalPrice: '39 490 som',
// },
// {
// trekId: 'TRK002',
// name: 'Filtr A2',
// weight: '3kg',
// quantity: 3,
// totalPrice: '39 490 som',
// },
// {
// trekId: 'TRK002',
// name: 'Filtr A2',
// weight: '3kg',
// quantity: 3,
// totalPrice: '39 490 som',
// },
// {
// trekId: 'TRK002',
// name: 'Filtr A2',
// weight: '3kg',
// quantity: 3,
// totalPrice: '39 490 som',
// },
// {
// trekId: 'TRK002',
// name: 'Filtr A2',
// weight: '3kg',
// quantity: 3,
// totalPrice: '39 490 som',
// },
// {
// trekId: 'TRK002',
// name: 'Filtr A2',
// weight: '3kg',
// quantity: 3,
// totalPrice: '39 490 som',
// },
// {
// trekId: 'TRK002',
// name: 'Filtr A2',
// weight: '3kg',
// quantity: 3,
// totalPrice: '39 490 som',
// },
// {
// trekId: 'TRK002',
// name: 'Filtr A2',
// weight: '3kg',
// quantity: 3,
// totalPrice: '39 490 som',
// },
// {
// trekId: 'TRK002',
// name: 'Filtr A2',
// weight: '3kg',
// quantity: 3,
// totalPrice: '39 490 som',
// },
// {
// trekId: 'TRK002',
// name: 'Filtr A2',
// weight: '3kg',
// quantity: 3,
// totalPrice: '39 490 som',
// },
// {
// trekId: 'TRK002',
// name: 'Filtr A2',
// weight: '3kg',
// quantity: 3,
// totalPrice: '39 490 som',
// },
// {
// trekId: 'TRK002',
// name: 'Filtr A2',
// weight: '3kg',
// quantity: 3,
// totalPrice: '39 490 som',
// },
// ],
// },
// {
// status: 'way',
// reys: 'REYS-A3',
// weight: '22kg',
// price: '143 450 som',
// cargo: 'auto',
// products: [
// {
// trekId: 'TRK003',
// name: 'Yuk qutisi A2',
// weight: '10kg',
// quantity: 2,
// totalPrice: '95 300 som',
// },
// {
// trekId: 'TRK004',
// name: 'Polietilen paket',
// weight: '2kg',
// quantity: 2,
// totalPrice: '48 150 som',
// },
// ],
// },
// {
// status: 'warehouse',
// reys: 'REYS-A1',
// weight: '18kg',
// price: '118 470 som',
// cargo: 'auto',
// products: [
// {
// trekId: 'TRK001',
// name: 'Avto detal A1',
// weight: '6kg',
// quantity: 2,
// totalPrice: '78 980 som',
// },
// {
// trekId: 'TRK002',
// name: 'Filtr A2',
// weight: '3kg',
// quantity: 3,
// totalPrice: '39 490 som',
// },
// {
// trekId: 'TRK002',
// name: 'Filtr A2',
// weight: '3kg',
// quantity: 3,
// totalPrice: '39 490 som',
// },
// {
// trekId: 'TRK002',
// name: 'Filtr A2',
// weight: '3kg',
// quantity: 3,
// totalPrice: '39 490 som',
// },
// {
// trekId: 'TRK002',
// name: 'Filtr A2',
// weight: '3kg',
// quantity: 3,
// totalPrice: '39 490 som',
// },
// {
// trekId: 'TRK002',
// name: 'Filtr A2',
// weight: '3kg',
// quantity: 3,
// totalPrice: '39 490 som',
// },
// {
// trekId: 'TRK002',
// name: 'Filtr A2',
// weight: '3kg',
// quantity: 3,
// totalPrice: '39 490 som',
// },
// {
// trekId: 'TRK002',
// name: 'Filtr A2',
// weight: '3kg',
// quantity: 3,
// totalPrice: '39 490 som',
// },
// {
// trekId: 'TRK002',
// name: 'Filtr A2',
// weight: '3kg',
// quantity: 3,
// totalPrice: '39 490 som',
// },
// {
// trekId: 'TRK002',
// name: 'Filtr A2',
// weight: '3kg',
// quantity: 3,
// totalPrice: '39 490 som',
// },
// {
// trekId: 'TRK002',
// name: 'Filtr A2',
// weight: '3kg',
// quantity: 3,
// totalPrice: '39 490 som',
// },
// {
// trekId: 'TRK002',
// name: 'Filtr A2',
// weight: '3kg',
// quantity: 3,
// totalPrice: '39 490 som',
// },
// {
// trekId: 'TRK002',
// name: 'Filtr A2',
// weight: '3kg',
// quantity: 3,
// totalPrice: '39 490 som',
// },
// {
// trekId: 'TRK002',
// name: 'Filtr A2',
// weight: '3kg',
// quantity: 3,
// totalPrice: '39 490 som',
// },
// {
// trekId: 'TRK002',
// name: 'Filtr A2',
// weight: '3kg',
// quantity: 3,
// totalPrice: '39 490 som',
// },
// {
// trekId: 'TRK002',
// name: 'Filtr A2',
// weight: '3kg',
// quantity: 3,
// totalPrice: '39 490 som',
// },
// ],
// },
// {
// status: 'customs',
// reys: 'REYS-A2',
// weight: '28kg',
// price: '165 000 som',
// cargo: 'auto',
// products: [
// {
// trekId: 'TRK005',
// name: 'Kiyimlar A3',
// weight: '10kg',
// quantity: 1,
// totalPrice: '55 000 som',
// },
// {
// trekId: 'TRK006',
// name: 'Sumka',
// weight: '8kg',
// quantity: 2,
// totalPrice: '110 000 som',
// },
// ],
// },
// {
// status: 'customs',
// reys: 'REYS-A4',
// weight: '16kg',
// price: '97 500 som',
// cargo: 'auto',
// products: [
// {
// trekId: 'TRK007',
// name: 'Elektronika V1',
// weight: '4kg',
// quantity: 2,
// totalPrice: '65 000 som',
// },
// {
// trekId: 'TRK008',
// name: 'Adapter',
// weight: '2kg',
// quantity: 2,
// totalPrice: '32 500 som',
// },
// ],
// },
// {
// status: 'delivery',
// reys: 'REYS-A5',
// weight: '28kg',
// price: '149 400 som',
// cargo: 'auto',
// products: [
// {
// trekId: 'TRK009',
// name: 'Kitoblar V2',
// weight: '5kg',
// quantity: 2,
// totalPrice: '71 300 som',
// },
// {
// trekId: 'TRK010',
// name: 'Daftar',
// weight: '3kg',
// quantity: 2,
// totalPrice: '53 400 som',
// },
// {
// trekId: 'TRK011',
// name: 'Ruchka toplami',
// weight: '2kg',
// quantity: 2,
// totalPrice: '17 800 som',
// },
// ],
// },
// {
// status: 'accepted',
// reys: 'REYS-A6',
// weight: '30kg',
// price: '110 000 som',
// cargo: 'auto',
// products: [
// {
// trekId: 'TRK012',
// name: 'Sport anjomlari V3',
// weight: '10kg',
// quantity: 2,
// totalPrice: '110 000 som',
// },
// ],
// },
// // AVIA
// {
// status: 'gathering',
// reys: 'REYS-V1',
// weight: '18kg',
// price: '108 000 som',
// cargo: 'avia',
// products: [
// {
// trekId: 'TRK013',
// name: 'Aksessuarlar',
// weight: '8kg',
// quantity: 1,
// totalPrice: '36 000 som',
// },
// {
// trekId: 'TRK014',
// name: 'Soat',
// weight: '5kg',
// quantity: 2,
// totalPrice: '72 000 som',
// },
// ],
// },
// {
// status: 'way',
// reys: 'REYS-V3',
// weight: '12kg',
// price: '84 000 som',
// cargo: 'avia',
// products: [
// {
// trekId: 'TRK015',
// name: 'Telefon qopqogi',
// weight: '6kg',
// quantity: 2,
// totalPrice: '42 000 som',
// },
// {
// trekId: 'TRK016',
// name: 'Quvvatlagich',
// weight: '6kg',
// quantity: 2,
// totalPrice: '42 000 som',
// },
// ],
// },
// {
// status: 'delivery',
// reys: 'REYS-V5',
// weight: '20kg',
// price: '100 000 som',
// cargo: 'avia',
// products: [
// {
// trekId: 'TRK017',
// name: 'Parfyum',
// weight: '10kg',
// quantity: 2,
// totalPrice: '100 000 som',
// },
// ],
// },
// ];

View File

@@ -0,0 +1,250 @@
import { PacketsData } from 'api/packets';
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
StyleSheet,
Text,
TouchableOpacity,
View,
useWindowDimensions,
} from 'react-native';
import CloseIcon from 'svg/Close';
import FilterIcon from 'svg/Filter';
const transportTypes = [
// 'all',
'AUTO',
'AVIA',
] as const;
type TransportType = (typeof transportTypes)[number];
interface Props {
transportTypes: TransportType;
setTransportTypes: (val: TransportType) => void;
reys: string;
data: PacketsData;
setReys: (val: string) => void;
setSelectedData: (val: any) => void;
}
const Filter = ({
transportTypes: selectedType,
setTransportTypes,
data,
reys: selectedFlight,
setReys,
setSelectedData,
}: Props) => {
const [open, setOpen] = React.useState(false);
const { width: screenWidth } = useWindowDimensions();
const { t } = useTranslation();
const scale = screenWidth < 360 ? 0.85 : 1;
const styles = makeStyles(scale);
const newOrders = React.useMemo(
() => data.data.filter(item => item.paymentStatus === 'NEW'),
[data],
);
return (
<View style={styles.container}>
<TouchableOpacity
style={styles.card}
onPress={() => setOpen(prev => !prev)}
>
<FilterIcon color="#000000" width={18 * scale} height={18 * scale} />
<Text style={styles.text}>{t('Filter')}</Text>
</TouchableOpacity>
{open && (
<View style={styles.dropdown}>
<View
style={{
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 10,
}}
>
<Text style={styles.sectionTitle}>{t('Transport')}</Text>
<TouchableOpacity onPress={() => setOpen(false)}>
<CloseIcon />
</TouchableOpacity>
</View>
<View style={styles.typeList}>
{transportTypes.map(type => (
<TouchableOpacity
key={type}
style={[
styles.typeButton,
selectedType === type && styles.activeType,
]}
onPress={() => {
setTransportTypes(type);
setOpen(false);
}}
>
<Text
style={[
styles.typeText,
selectedType === type && styles.activeTypeText,
]}
>
{
// type === 'all'
// ? t('Barchasi')
// :
type === 'AUTO' ? t('Avto') : t('Avia')
}
</Text>
</TouchableOpacity>
))}
</View>
<Text style={styles.sectionTitle}>{t('Reys raqami')}</Text>
<View style={styles.flightList}>
<TouchableOpacity
style={[
styles.flightButton,
selectedFlight === 'all' && styles.activeFlight,
]}
onPress={() => {
setReys('all');
setSelectedData(null);
setOpen(false);
}}
>
<Text
style={[
styles.flightText,
selectedFlight === 'all' && styles.activeFlightText,
]}
>
{t('Barchasi')}
</Text>
</TouchableOpacity>
{newOrders.map(item => (
<TouchableOpacity
key={item.id}
style={[
styles.flightButton,
selectedFlight === item.packetName && styles.activeFlight,
]}
onPress={() => {
setReys(item.packetName);
setSelectedData(item);
setOpen(false);
}}
>
<Text
style={[
styles.flightText,
selectedFlight === item.packetName &&
styles.activeFlightText,
]}
>
{item.packetName}
</Text>
</TouchableOpacity>
))}
</View>
</View>
)}
</View>
);
};
const makeStyles = (scale: number) =>
StyleSheet.create({
container: {
height: 'auto',
borderRadius: 8 * scale,
alignItems: 'flex-end',
justifyContent: 'flex-start',
position: 'relative',
zIndex: 10,
},
card: {
paddingHorizontal: 12 * scale,
height: 40 * scale,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#D8DADC',
borderRadius: 8 * scale,
flexDirection: 'row',
gap: 4 * scale,
},
text: {
color: '#000000',
fontWeight: '500',
fontSize: 14 * scale,
},
dropdown: {
position: 'absolute',
top: 50 * scale,
right: 0,
backgroundColor: '#fff',
borderRadius: 8 * scale,
paddingVertical: 8 * scale,
paddingHorizontal: 10 * scale,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 4,
elevation: 5,
zIndex: 10,
minWidth: 200 * scale,
},
sectionTitle: {
fontWeight: '600',
marginBottom: 6 * scale,
color: '#333',
fontSize: 16 * scale,
},
typeList: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8 * scale,
marginBottom: 12 * scale,
},
typeButton: {
backgroundColor: '#F3FAFF',
paddingHorizontal: 5 * scale,
paddingVertical: 6 * scale,
borderRadius: 6 * scale,
},
activeType: {
backgroundColor: '#28A7E8',
},
typeText: {
color: '#28A7E8',
fontWeight: '500',
fontSize: 14 * scale,
},
activeTypeText: {
color: '#fff',
},
flightList: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8 * scale,
marginBottom: 12 * scale,
},
flightButton: {
backgroundColor: '#F3FAFF',
paddingHorizontal: 10 * scale,
paddingVertical: 6 * scale,
borderRadius: 6 * scale,
},
activeFlight: {
backgroundColor: '#28A7E8',
},
flightText: {
color: '#28A7E8',
fontWeight: '500',
fontSize: 14 * scale,
},
activeFlightText: {
color: '#fff',
},
});
export default Filter;

View File

@@ -0,0 +1,323 @@
import { PacketsData } from 'api/packets';
import React, { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import {
StyleSheet,
Text,
TouchableOpacity,
View,
useWindowDimensions,
} from 'react-native';
import Auto from 'svg/Auto';
import Avia from 'svg/Avia';
import BagIcon from 'svg/BagIcon';
import BoxIcon from 'svg/Box';
import Store from 'svg/Store';
import SuccessIcon from 'svg/SuccessIcon';
import TrunkIcon from 'svg/TrunkIcon';
import { DataInfo } from '../lib/data';
const statusColorMap: Record<string, string> = {
COLLECTING: '#FF69B4',
ON_THE_WAY: '#1E90FF',
IN_CUSTOMS: '#8A2BE2',
IN_WAREHOUSE: '#00BFFF',
ARRIVED: '#FFA500',
DELIVERED: '#32CD32',
};
const statuses = [
'COLLECTING',
'ON_THE_WAY',
'IN_CUSTOMS',
'IN_WAREHOUSE',
'ARRIVED',
'DELIVERED',
'PAID',
] as const;
type FilterType = (typeof statuses)[number];
const tabList: { label: string; value: FilterType }[] = [
{ label: "Yig'ilmoqda", value: 'COLLECTING' },
{ label: "Yo'lda", value: 'ON_THE_WAY' },
{ label: 'Bojxonada', value: 'IN_CUSTOMS' },
{ label: 'Toshkent omboriga yetib keldi', value: 'IN_WAREHOUSE' },
{ label: 'Topshirish punktida', value: 'DELIVERED' },
{ label: 'Qabul qilingan', value: 'PAID' },
];
interface Props {
data: PacketsData;
selectedData: DataInfo | null;
openModal: (item: DataInfo) => void;
}
const Order = ({ data, openModal, selectedData }: Props) => {
const { width: screenWidth } = useWindowDimensions();
const scale = useMemo(() => (screenWidth < 360 ? 0.85 : 1), [screenWidth]);
const { t } = useTranslation();
const styles = useMemo(
() => makeStyles(scale, screenWidth),
[scale, screenWidth],
);
const createIcons = useCallback(
(scale: number, cargo: string) => [
{
status: 'COLLECTING',
icon: <BoxIcon width={20 * scale} height={20 * scale} color="" />,
},
{
status: 'ON_THE_WAY',
icon:
cargo === 'avia' ? (
<Avia width={20 * scale} height={20 * scale} color="" />
) : (
<Auto width={20 * scale} height={20 * scale} color="" view="-4" />
),
},
{
status: 'IN_CUSTOMS',
icon: <BagIcon width={20 * scale} height={20 * scale} color="" />,
},
{
status: 'IN_WAREHOUSE',
icon: <Store width={20 * scale} height={20 * scale} color="" />,
},
{
status: 'DELIVERED',
icon: <TrunkIcon width={20 * scale} height={20 * scale} color="" />,
},
{
status: 'PAID',
icon: <SuccessIcon width={20 * scale} height={20 * scale} color="" />,
},
],
[],
);
const handleItemPress = useCallback(
(item: DataInfo) => {
openModal(item);
},
[openModal],
);
const renderOrderItem = useCallback(
(ItemLayout: DataInfo, index: number) => {
const currentStatusIndex = statuses.indexOf(
ItemLayout.deliveryStatus as FilterType,
);
const icons = createIcons(scale, ItemLayout.deliveryStatus);
return (
<TouchableOpacity
key={index}
onPress={() => handleItemPress(ItemLayout)}
>
<View style={styles.card}>
<View style={styles.statusCard}>
{icons.map((item, i) => {
const iconColor = i <= currentStatusIndex ? '#28A7E8' : '#000';
const viewColor =
i <= currentStatusIndex ? '#28A7E81A' : '#0000001A';
return (
<React.Fragment key={item.status}>
<View
style={[styles.circle, { backgroundColor: viewColor }]}
>
{React.cloneElement(item.icon, { color: iconColor })}
</View>
{i !== icons.length - 1 && (
<View
style={[
styles.divider,
{ borderBottomColor: iconColor },
]}
/>
)}
</React.Fragment>
);
})}
</View>
<View
style={[
styles.statusLabelWrapper,
{
backgroundColor: `${
statusColorMap[ItemLayout.deliveryStatus]
}1A`,
},
]}
>
<Text
style={[
styles.statusText,
{ color: statusColorMap[ItemLayout.deliveryStatus] },
]}
>
{t(
tabList.find(item => item.value === ItemLayout.deliveryStatus)
?.label || '',
)}
</Text>
</View>
<View style={styles.infoCard}>
<Text style={styles.infoTitle}>{t('Reys raqami')}</Text>
<Text
style={[styles.infoText, { width: '50%', textAlign: 'right' }]}
>
{ItemLayout.packetName}
</Text>
</View>
<View style={styles.infoCard}>
<Text style={styles.infoTitle}>{t('Mahsulotlar ogirligi')}</Text>
<Text style={styles.infoText}>{ItemLayout.weight}</Text>
</View>
<View style={styles.infoCard}>
<Text style={styles.infoTitle}>{t('Umumiy narxi')}</Text>
<Text style={styles.infoText}>{ItemLayout.totalPrice}</Text>
</View>
</View>
</TouchableOpacity>
);
},
[scale, createIcons, handleItemPress],
);
const orderItems = useMemo(() => {
if (selectedData) {
return [renderOrderItem(selectedData, 0)];
}
return data.data.map(renderOrderItem);
}, [data, renderOrderItem, selectedData]);
const countSection = useMemo(
() => (
<View style={styles.count}>
<Text style={styles.title}>{t('Buyurtmalar soni')}</Text>
{selectedData ? (
<Text style={styles.title}>1</Text>
) : (
<Text style={styles.title}>{data.data.length}</Text>
)}
</View>
),
[styles.count, styles.title, data.data.length, selectedData],
);
return (
<View style={styles.container}>
{countSection}
{orderItems}
</View>
);
};
const makeStyles = (scale: number, screenWidth: number) =>
StyleSheet.create({
container: {
width: screenWidth * 0.95,
marginTop: 10,
alignSelf: 'center',
borderRadius: 8 * scale,
padding: 8 * scale,
backgroundColor: '#F5F5F5',
},
count: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
title: {
fontSize: 16 * scale,
fontWeight: '500',
color: '#333',
},
card: {
backgroundColor: '#FFFFFF',
marginTop: 12 * scale,
padding: 15 * scale,
borderRadius: 10 * scale,
gap: 5 * scale,
flexDirection: 'column',
},
statusCard: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 10 * scale,
},
circle: {
padding: 8 * scale,
borderRadius: 50,
alignItems: 'center',
justifyContent: 'center',
},
divider: {
width: 20 * scale,
borderBottomWidth: 2 * scale,
borderStyle: 'dashed',
},
statusLabelWrapper: {
borderRadius: 8 * scale,
alignSelf: 'flex-start',
paddingVertical: 6 * scale,
paddingHorizontal: 10 * scale,
marginBottom: 12 * scale,
},
statusText: {
fontWeight: '600',
fontSize: 16 * scale,
},
infoCard: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 6 * scale,
},
infoTitle: {
fontSize: 16 * scale,
color: '#979797',
fontWeight: '500',
},
infoText: {
fontSize: 14 * scale,
color: '#28A7E8',
fontWeight: '500',
},
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.4)',
justifyContent: 'center',
alignItems: 'center',
zIndex: 20,
},
modalContent: {
width: '90%',
maxHeight: '80%',
backgroundColor: '#fff',
borderRadius: 10,
padding: 20,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 4,
elevation: 5,
},
modalTitle: {
fontSize: 20 * scale,
fontWeight: '600',
marginBottom: 10,
},
closeButton: {
marginTop: 20,
backgroundColor: '#28A7E8',
paddingVertical: 10,
borderRadius: 8,
alignItems: 'center',
},
});
export default Order;

View File

@@ -0,0 +1,247 @@
import React, { useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import {
Animated,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import CloseIcon from 'svg/Close';
import { DataInfo } from '../lib/data';
interface Props {
visible: boolean;
setVisible: (val: boolean) => void;
selectedOrder: DataInfo | null;
}
const OrderDetailModal = ({ visible, setVisible, selectedOrder }: Props) => {
const opacity = useRef(new Animated.Value(0)).current;
const translateY = useRef(new Animated.Value(50)).current;
const { t } = useTranslation();
const parsePrice = (priceStr: string) => {
return Number(priceStr.replace(/[^\d]/g, ''));
};
const parseWeight = (weightStr: string) => {
return Number(weightStr.replace(/[^\d.]/g, ''));
};
const closeModal = () => {
Animated.timing(opacity, {
toValue: 0,
duration: 200,
useNativeDriver: true,
}).start(() => setVisible(false));
};
useEffect(() => {
if (visible) {
Animated.parallel([
Animated.timing(opacity, {
toValue: 1,
duration: 250,
useNativeDriver: true,
}),
Animated.spring(translateY, {
toValue: 0,
useNativeDriver: true,
}),
]).start();
}
}, [visible]);
if (!visible || !selectedOrder) return null;
return (
<View style={styles.overlay}>
<Animated.View
style={[
styles.modalContent,
{
opacity: opacity,
transform: [{ translateY }],
},
]}
>
<View style={styles.header}>
<Text style={styles.title}>{selectedOrder.packetName}</Text>
<TouchableOpacity onPress={closeModal} style={styles.closeBtn}>
<CloseIcon width={15} height={15} color={'#000'} />
</TouchableOpacity>
</View>
<View style={styles.divider} />
<Text style={styles.sectionTitle}>{t('Mahsulotlar')}:</Text>
<ScrollView
showsVerticalScrollIndicator={false}
style={{ maxHeight: 250 }}
>
{selectedOrder.items.map((product, index) => {
const totalPrice = product.totalPrice;
const weight = product.weight;
const pricePerKg = Math.ceil(weight ? totalPrice / weight : 0);
return (
<View key={product.trekId + index} style={styles.productItem}>
<View style={styles.row}>
<Text style={styles.productName}>{product.name}</Text>
<Text style={styles.productTrekId}>{t('Trek ID')}:</Text>
</View>
<Text style={styles.productTrekId}>{product.trekId}</Text>
<View style={styles.row}>
<Text style={styles.detail}>
{t('Ogirligi')}: {product.weight}
</Text>
<Text style={styles.detail}>
{t('Narxi')}: 1kg * {pricePerKg.toLocaleString('uz-UZ')}{' '}
{t("so'm")}
</Text>
</View>
<View style={styles.rowRight}>
<Text style={styles.total}>
{t('Umumiy narxi')}: {product.totalPrice} {t('som')}
</Text>
</View>
</View>
);
})}
</ScrollView>
<View style={styles.totalRow}>
<Text style={styles.totalLabel}>{t('Umumiy narx')}:</Text>
<Text style={styles.totalValue}>{selectedOrder.totalPrice}</Text>
</View>
<TouchableOpacity onPress={closeModal} style={styles.btn}>
<Text style={styles.btnText}>{t('Yopish')}</Text>
</TouchableOpacity>
</Animated.View>
</View>
);
};
const styles = StyleSheet.create({
overlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0,0,0,0.4)',
justifyContent: 'center',
alignItems: 'center',
zIndex: 100,
},
rowRight: {
flexDirection: 'row',
justifyContent: 'flex-end',
marginTop: 5,
},
total: {
fontSize: 14,
fontWeight: '600',
color: '#1D1D1D',
},
modalContent: {
width: '90%',
backgroundColor: '#fff',
borderRadius: 10,
paddingBottom: 20,
shadowColor: '#000',
shadowOpacity: 0.3,
shadowRadius: 4,
elevation: 6,
},
header: {
padding: 20,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
title: {
fontSize: 18,
fontWeight: '600',
},
closeBtn: {
padding: 5,
width: 30,
height: 30,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#0000000D',
borderRadius: 50,
},
divider: {
height: 1,
backgroundColor: '#E0E0E0',
marginBottom: 10,
},
sectionTitle: {
fontSize: 16,
fontWeight: '600',
paddingHorizontal: 20,
marginBottom: 8,
},
productItem: {
paddingHorizontal: 20,
paddingBottom: 12,
borderBottomWidth: 1,
borderBottomColor: '#eee',
marginBottom: 10,
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
},
productName: {
fontWeight: '500',
fontSize: 15,
},
productTrekId: {
color: '#555',
textAlign: 'right',
},
detail: {
color: '#555',
marginTop: 8,
},
totalRow: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingHorizontal: 20,
paddingTop: 10,
borderTopWidth: 1,
borderTopColor: '#eee',
},
totalLabel: {
fontSize: 16,
fontWeight: '600',
},
totalValue: {
fontSize: 16,
fontWeight: '600',
color: '#28A7E8',
},
btn: {
alignSelf: 'center',
marginTop: 20,
height: 48,
width: '90%',
borderRadius: 8,
justifyContent: 'center',
backgroundColor: '#28A7E8',
},
btnText: {
textAlign: 'center',
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
});
export default OrderDetailModal;

View File

@@ -0,0 +1,286 @@
import { useQuery } from '@tanstack/react-query';
import packetsApi from 'api/packets';
import LoadingScreen from 'components/LoadingScreen';
import Navbar from 'components/Navbar';
import Navigation from 'components/Navigation';
import NoResult from 'components/NoResult';
import Pagination from 'components/Pagination';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Animated,
RefreshControl,
ScrollView,
StyleSheet,
View,
useWindowDimensions,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import Serach from 'svg/Serach';
import { DataInfo } from '../lib/data';
import Filter from './Filter';
import Order from './Order';
import OrderDetailModal from './OrderDetailModal';
import Tabs from './Tabs';
const Status = () => {
const { width: screenWidth } = useWindowDimensions();
const scale = screenWidth < 360 ? 0.85 : 1;
const [filter, setFilter] = useState<
| 'COLLECTING'
| 'ON_THE_WAY'
| 'IN_CUSTOMS'
| 'IN_WAREHOUSE'
| 'DELIVERED'
| 'PAID'
>('COLLECTING');
const [selectedOrder, setSelectedOrder] = useState<DataInfo | null>(null);
const [transportTypes, setTransportTypes] = useState<
// 'all'|
'AUTO' | 'AVIA'
>('AUTO');
const [page, setPage] = useState(0);
const {
data: statusData,
refetch,
isLoading,
isFetching,
} = useQuery({
queryKey: ['status', filter, transportTypes, page],
queryFn: () =>
packetsApi.getPacketsStatus(filter, {
page,
size: 10,
cargoType: transportTypes,
}),
});
const [modalVisible, setModalVisible] = useState(false);
const scaleAnim = React.useRef(new Animated.Value(0.8)).current;
const opacityAnim = React.useRef(new Animated.Value(0)).current;
const styles = useMemo(() => makeStyles(scale), [scale]);
const [loadingMessage, setLoadingMessage] = useState(
"Ma'lumotlar yuklanmoqda...",
);
const [progress, setProgress] = useState(0);
const [isDataLoaded, setIsDataLoaded] = useState(false);
useEffect(() => {
if (!isDataLoaded) {
const start = Date.now();
const duration = 1000;
const interval = setInterval(() => {
const elapsed = Date.now() - start;
const progressValue = Math.min(elapsed / duration, 1);
setProgress(progressValue);
if (progressValue >= 1) clearInterval(interval);
}, 100);
setTimeout(() => setIsDataLoaded(true), duration);
}
}, [isDataLoaded]);
useEffect(() => {
if (modalVisible) {
scaleAnim.setValue(0.8);
opacityAnim.setValue(0);
Animated.parallel([
Animated.timing(scaleAnim, {
toValue: 1,
duration: 300,
useNativeDriver: true,
}),
Animated.timing(opacityAnim, {
toValue: 1,
duration: 300,
useNativeDriver: true,
}),
]).start();
}
}, [modalVisible, scaleAnim, opacityAnim]);
const openModal = useCallback((item: DataInfo) => {
setSelectedOrder(item);
setModalVisible(true);
}, []);
const [refreshing, setRefreshing] = useState(false);
const [reys, setReys] = useState<string>('all');
const [selectedData, setSelectedData] = useState<DataInfo | null>(null);
const { t } = useTranslation();
const onRefresh = useCallback(async () => {
setRefreshing(true);
try {
await refetch();
} finally {
setRefreshing(false);
}
}, [refetch]);
const refreshControl = useMemo(
() => (
<RefreshControl
refreshing={refreshing || isFetching}
onRefresh={onRefresh}
/>
),
[refreshing, isFetching, onRefresh],
);
const searchIcon = useMemo(
() => <Serach color="#D8DADC" width={20 * scale} height={20 * scale} />,
[scale],
);
if (isLoading || isFetching) {
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={styles.container}>
<Navbar />
<LoadingScreen message={loadingMessage} progress={progress} />
<Navigation />
</View>
</SafeAreaView>
);
}
if (statusData?.data.length === 0) {
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={styles.container}>
<Navbar />
<Tabs filter={filter} setFilter={setFilter} />
<View style={styles.controls}>
{/* <View style={styles.searchContainer}>
<TextInput
placeholder={t('ID orqali izlash')}
placeholderTextColor="#D8DADC"
style={styles.search}
/>
<View style={styles.searchIcon}>{searchIcon}</View>
</View> */}
<View style={{ position: 'relative' }}>
<Filter
transportTypes={transportTypes}
setTransportTypes={setTransportTypes}
reys={reys}
setReys={setReys}
data={statusData!}
setSelectedData={setSelectedData}
/>
</View>
</View>
<NoResult message={t("Hech qanday ma'lumot topilmadi")} />
<Navigation />
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={styles.container}>
<Navbar />
<ScrollView
keyboardShouldPersistTaps="handled"
refreshControl={refreshControl}
removeClippedSubviews={true}
>
<Tabs filter={filter} setFilter={setFilter} />
<View style={styles.controls}>
{/* <View style={styles.searchContainer}>
<TextInput
placeholder={t('ID orqali izlash')}
placeholderTextColor="#D8DADC"
style={styles.search}
/>
<View style={styles.searchIcon}>{searchIcon}</View>
</View> */}
<View style={{ position: 'relative' }}>
<Filter
transportTypes={transportTypes}
setTransportTypes={setTransportTypes}
reys={reys}
setReys={setReys}
data={statusData!}
setSelectedData={setSelectedData}
/>
</View>
</View>
<Order
data={statusData!}
openModal={openModal}
selectedData={selectedData}
/>
</ScrollView>
<OrderDetailModal
visible={modalVisible}
setVisible={setModalVisible}
selectedOrder={selectedOrder}
/>
<View
style={{
flexDirection: 'row',
justifyContent: 'flex-end',
width: '95%',
alignSelf: 'center',
paddingRight: 20,
paddingVertical: 10,
}}
>
<Pagination
page={page}
totalPages={statusData?.totalPages ?? 1}
setPage={setPage}
/>
</View>
<Navigation />
</View>
</SafeAreaView>
);
};
const makeStyles = (scale: number) =>
StyleSheet.create({
container: {
flex: 1,
},
controls: {
marginTop: 10,
paddingHorizontal: 10 * scale,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'flex-end',
},
searchContainer: {
flex: 1,
marginRight: 8 * scale,
position: 'relative',
},
search: {
width: '100%',
height: 40 * scale,
borderColor: '#D8DADC',
borderWidth: 1,
borderRadius: 8 * scale,
paddingLeft: 40 * scale,
paddingRight: 10 * scale,
color: '#000',
fontSize: 14 * scale,
},
searchIcon: {
position: 'absolute',
left: 15 * scale,
top: '50%',
transform: [{ translateY: -12 * scale }],
zIndex: 1,
},
});
export default Status;

View File

@@ -0,0 +1,144 @@
import React, { Dispatch, SetStateAction, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
NativeScrollEvent,
NativeSyntheticEvent,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
useWindowDimensions,
} from 'react-native';
import ArrowLeft from 'svg/ArrowLeft';
import ArrowRightUnderline from 'svg/ArrowRightUnderline';
type FilterType =
| 'COLLECTING'
| 'ON_THE_WAY'
| 'IN_CUSTOMS'
| 'IN_WAREHOUSE'
| 'DELIVERED'
| 'PAID';
interface Props {
filter: FilterType;
setFilter: Dispatch<SetStateAction<FilterType>>;
}
const tabList: { label: string; value: FilterType }[] = [
{ label: "Yig'ilmoqda", value: 'COLLECTING' },
{ label: "Yo'lda", value: 'ON_THE_WAY' },
{ label: 'Bojxonada', value: 'IN_CUSTOMS' },
{ label: 'Toshkent omboriga yetib keldi', value: 'IN_WAREHOUSE' },
{ label: 'Topshirish punktiga yuborildi', value: 'DELIVERED' },
// { label: 'Qabul qilingan', value: 'DELIVERED' },
];
const Tabs = ({ filter, setFilter }: Props) => {
const { width: screenWidth } = useWindowDimensions();
const scale = screenWidth < 360 ? 0.85 : 1;
const styles = makeStyles(scale);
const scrollRef = useRef<ScrollView>(null);
const [scrollX, setScrollX] = useState(0);
const { t } = useTranslation();
const scrollStep = 120;
const handleScrollLeft = () => {
if (scrollRef.current) {
scrollRef.current.scrollTo({
x: Math.max(0, scrollX - scrollStep),
animated: true,
});
}
};
const handleScrollRight = () => {
if (scrollRef.current) {
scrollRef.current.scrollTo({ x: scrollX + scrollStep, animated: true });
}
};
const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
setScrollX(event.nativeEvent.contentOffset.x);
};
return (
<View style={styles.container}>
<TouchableOpacity style={styles.navButton} onPress={handleScrollLeft}>
<ArrowLeft color="#28A7E8" width={20} height={20} />
</TouchableOpacity>
<ScrollView
horizontal
onScroll={onScroll}
ref={scrollRef}
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.scrollContent}
>
{tabList.map(tab => (
<TouchableOpacity
key={tab.value}
style={[styles.card, filter === tab.value && styles.activeCard]}
onPress={() => setFilter(tab.value)}
>
<Text
style={[styles.text, filter === tab.value && styles.activeText]}
>
{t(tab.label)}
</Text>
</TouchableOpacity>
))}
</ScrollView>
<TouchableOpacity style={styles.navButton} onPress={handleScrollRight}>
<ArrowRightUnderline color="#28A7E8" width={20} height={20} />
</TouchableOpacity>
</View>
);
};
const makeStyles = (scale: number) =>
StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
width: '95%',
height: 50 * scale,
backgroundColor: '#FFFFFF',
marginTop: 20 * scale,
alignSelf: 'center',
borderRadius: 8 * scale,
paddingHorizontal: 4 * scale,
},
scrollContent: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 4 * scale,
},
card: {
paddingHorizontal: 12 * scale,
paddingVertical: 8 * scale,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#F3FAFF',
marginRight: 8 * scale,
borderRadius: 8 * scale,
},
activeCard: {
backgroundColor: '#28A7E8',
},
text: {
color: '#28A7E8',
fontWeight: '500',
fontSize: 14 * scale,
},
activeText: {
color: '#fff',
},
navButton: {
padding: 6 * scale,
},
});
export default Tabs;

View File

@@ -0,0 +1,9 @@
import { z } from 'zod';
export const paymentSchema = z.object({
card_name: z.string().min(3, 'Majburiy maydon'),
card_number: z.string().min(16, 'Karta raqami xato'),
duration: z.string().length(5, 'Amal qilish muddati xato'),
});
export type PaymentFormType = z.infer<typeof paymentSchema>;

View File

@@ -0,0 +1,306 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { RouteProp, useRoute } from '@react-navigation/native';
import NavbarBack from 'components/NavbarBack';
import Navigation from 'components/Navigation';
import * as React from 'react';
import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import {
ActivityIndicator,
KeyboardAvoidingView,
Linking,
Platform,
ScrollView,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
import AppLink from 'react-native-app-link';
import MaskInput from 'react-native-mask-input';
import { SafeAreaView } from 'react-native-safe-area-context';
import { PaymentStyle } from 'screens/wallet/payment/ui/style';
import ModalSuccess from 'screens/wallet/paymentMethod/ui/ModalSuccess';
import { RootStackParamList } from 'types/types';
import { PaymentFormType, paymentSchema } from '../lib/form';
interface EnterCardProps {}
type EnterCardRouteProp = RouteProp<RootStackParamList, 'EnterCard'>;
const EnterCard = (props: EnterCardProps) => {
const {
control,
handleSubmit,
setValue,
formState: { errors },
} = useForm<PaymentFormType>({
resolver: zodResolver(paymentSchema),
defaultValues: {
card_name: '',
card_number: '',
duration: '',
},
});
const [load, setLoad] = React.useState(false);
const [success, setSuccess] = React.useState(false);
const [payModal, setPayModal] = React.useState(false);
const { t } = useTranslation();
const route = useRoute<EnterCardRouteProp>();
const { selectedId } = route.params;
const openPayme = React.useCallback(async () => {
try {
await AppLink.maybeOpenURL('https://checkout.paycom.uz/openapp', {
appName: 'Payme',
appStoreId: 1093525667,
appStoreLocale: 'uz',
playStoreId: 'uz.dida.payme',
});
} catch (err) {
Linking.openURL('https://checkout.paycom.uz/openapp');
}
}, []);
const openClick = async () => {
try {
await AppLink.maybeOpenURL('click://my.click.uz/clickp2p/some-id', {
appName: 'Payme',
appStoreId: 1093525667,
appStoreLocale: 'uz',
playStoreId: 'uz.dida.payme',
});
} catch (err) {
Linking.openURL('click://');
}
};
const onSubmit = (data: PaymentFormType) => {
setLoad(true);
setTimeout(() => {
setSuccess(true);
setLoad(false);
}, 3000);
};
const openApp = () => {
setTimeout(() => {
if (selectedId === 'payme') {
openPayme();
} else if (selectedId === 'click') {
openClick();
}
}, 3000);
};
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={{ flex: 1 }}>
<NavbarBack title={t("To'lov usuli")} />
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView
style={[PaymentStyle.containerMethod]}
contentContainerStyle={{ paddingBottom: 80 }}
>
<View style={{ gap: 20 }}>
<Controller
control={control}
name="card_name"
render={({ field: { onChange, value } }) => (
<View>
<Text style={PaymentStyle.label}>{t('Karta nomi')}</Text>
<TextInput
style={PaymentStyle.input}
placeholder={t('Karta nomi')}
placeholderTextColor={'#D8DADC'}
onChangeText={onChange}
value={value}
/>
{errors.card_name && (
<Text style={PaymentStyle.errorText}>
{t(errors.card_name.message || '')}
</Text>
)}
</View>
)}
/>
<Controller
control={control}
name="card_number"
render={({ field: { onChange, value } }) => {
const rawDigits = value.replace(/\D+/g, '');
const formattedValue = value;
const ghostMask = '0000 0000 0000 0000';
const ghostRemaining = ghostMask.slice(formattedValue.length);
return (
<View>
<Text style={PaymentStyle.label}>
{t('Karta raqami')}
</Text>
<View style={{ position: 'relative' }}>
{/* Ghost text with value + remaining */}
<Text
style={[
PaymentStyle.input,
{
position: 'absolute',
top: 0,
left: 0,
color: '#D8DADC',
width: '100%',
height: '100%',
zIndex: 1,
padding: 15,
fontSize: 16,
fontWeight: '400',
pointerEvents: 'none', // 👈 Shart! Shunda cursor ustidan chiqmaydi
},
]}
>
<Text style={{ color: '#000' }}>
{formattedValue}
</Text>
<Text>{ghostRemaining}</Text>
</Text>
<MaskInput
value={value}
onChangeText={masked => {
onChange(masked);
}}
mask={[
/\d/,
/\d/,
/\d/,
/\d/,
' ',
/\d/,
/\d/,
/\d/,
/\d/,
' ',
/\d/,
/\d/,
/\d/,
/\d/,
' ',
/\d/,
/\d/,
/\d/,
/\d/,
]}
keyboardType="number-pad"
style={[
PaymentStyle.input,
{
color: 'rgba(0,0,0,0)',
zIndex: 2,
backgroundColor: 'transparent',
},
]}
caretHidden={false}
selectionColor="#000"
placeholder=""
/>
</View>
{errors.card_number && (
<Text style={PaymentStyle.errorText}>
{t(errors.card_number.message || '')}
</Text>
)}
</View>
);
}}
/>
<Controller
control={control}
name="duration"
render={({ field: { onChange, value } }) => {
// Raqamlarni olib, 2-raqamdan keyin "/" belgisini qoshish
const formatDuration = (text: string) => {
const cleaned = text.replace(/\D+/g, '').slice(0, 4); // Faqat raqamlar, maksimal 4 ta
if (cleaned.length >= 3) {
return `${cleaned.slice(0, 2)}/${cleaned.slice(2)}`;
} else {
return cleaned;
}
};
const handleTextChange = (text: string) => {
const formatted = formatDuration(text);
onChange(formatted);
};
return (
<View>
<Text style={PaymentStyle.label}>{t('Muddati')}</Text>
<TextInput
style={PaymentStyle.input}
placeholder="MM/YY"
placeholderTextColor={'#D8DADC'}
onChangeText={handleTextChange}
value={value}
keyboardType="numeric"
maxLength={5}
/>
{errors.duration && (
<Text style={PaymentStyle.errorText}>
{t(errors.duration.message || '')}
</Text>
)}
</View>
);
}}
/>
</View>
<TouchableOpacity
onPress={openApp}
style={{
width: '100%',
justifyContent: 'flex-end',
alignItems: 'flex-end',
marginTop: 20,
}}
>
<Text>
{selectedId?.toUpperCase()} {t("ilovasi orqali to'lash")}
</Text>
</TouchableOpacity>
</ScrollView>
</KeyboardAvoidingView>
{success && (
<ModalSuccess
visible={success}
setVisible={setSuccess}
setPayModal={setPayModal}
/>
)}
<TouchableOpacity
style={{
borderRadius: 8,
justifyContent: 'center',
backgroundColor: '#28A7E8',
zIndex: 10,
marginBottom: 10,
width: '95%',
margin: 'auto',
height: 56,
}}
onPress={handleSubmit(onSubmit)}
disabled={load}
>
{load ? (
<ActivityIndicator size="small" color="#fff" />
) : (
<Text style={PaymentStyle.btnText}>{t("To'lash")}</Text>
)}
</TouchableOpacity>
<Navigation />
</View>
</SafeAreaView>
);
};
export default EnterCard;

View File

@@ -0,0 +1,45 @@
type PaymentStatus = 'paid' | 'unpaid';
interface PaymentData {
id: string;
reys: string;
weight: string;
count: number;
totalPrice: string;
status: PaymentStatus;
}
export const fakePayments: PaymentData[] = [
{
id: '1',
reys: 'Avia-CP-22',
weight: '10kg',
count: 5,
totalPrice: '78 980 som',
status: 'unpaid',
},
{
id: '2',
reys: 'Avia-CP-23',
weight: '8kg',
count: 3,
totalPrice: '52 000 som',
status: 'paid',
},
{
id: '3',
reys: 'Avia-CP-24',
weight: '15kg',
count: 7,
totalPrice: '100 500 som',
status: 'paid',
},
{
id: '4',
reys: 'Avia-CP-25',
weight: '12kg',
count: 6,
totalPrice: '88 200 som',
status: 'paid',
},
];

View File

@@ -0,0 +1,116 @@
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { PacketsData } from 'api/packets';
import React, { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Text, TouchableOpacity, View } from 'react-native';
import { PaymentStyle } from './style';
type WalletStackParamList = {
PaymentMethod: { packets: PacketsData };
PaymentQrCode: { packets: PacketsData };
};
type LoginScreenNavigationProp =
NativeStackNavigationProp<WalletStackParamList>;
interface Props {
packets: PacketsData;
}
const Payment = ({ packets }: Props) => {
const navigation = useNavigation<LoginScreenNavigationProp>();
const { t } = useTranslation();
const handlePaymentPress = useCallback(
(item: any) => {
const isPaid = item.paymentStatus === 'paid';
navigation.navigate(isPaid ? 'PaymentQrCode' : 'PaymentMethod', {
packets: item, // tanlangan itemni toliq yuboramiz
});
},
[navigation],
);
const cardContainerStyle = useMemo(
() => ({
flexDirection: 'row' as const,
gap: 10,
justifyContent: 'center' as const,
alignItems: 'center' as const,
}),
[],
);
const badgeStyle = useMemo(
() => [PaymentStyle.badge, { backgroundColor: '#D32F2F' }],
[],
);
const renderPaymentCard = useCallback(
(item: any) => {
const isPaid = item.paymentStatus === 'paid';
const cardStyle = [
PaymentStyle.card,
{ borderColor: isPaid ? '#4CAF50' : '#D32F2F', borderWidth: 1.5 },
];
return (
<TouchableOpacity
activeOpacity={0.7}
onPress={() => handlePaymentPress(item)}
key={item.id}
>
<View style={cardContainerStyle}>
<View style={cardStyle}>
<View style={PaymentStyle.cardHeader}>
<Text style={PaymentStyle.title}>{item.packetName}</Text>
{isPaid ? (
<Text style={PaymentStyle.badge}>{t("To'langan")}</Text>
) : (
<Text style={badgeStyle}>{t("To'lanmagan")}</Text>
)}
</View>
<View style={PaymentStyle.row}>
<Text style={PaymentStyle.infoTitle}>{t('Reys raqami')}</Text>
<Text
style={[
PaymentStyle.text,
{ width: '60%', textAlign: 'right' },
]}
>
{item.packetName}
</Text>
</View>
<View style={PaymentStyle.row}>
<Text style={PaymentStyle.infoTitle}>
{t("Mahsulotlar og'irligi")}
</Text>
<Text style={PaymentStyle.text}>{item.weight}</Text>
</View>
<View style={PaymentStyle.row}>
<Text style={PaymentStyle.infoTitle}>
{t('Mahsulotlar soni')}
</Text>
<Text style={PaymentStyle.text}>{item.items.length}</Text>
</View>
<View style={PaymentStyle.row}>
<Text style={PaymentStyle.infoTitle}>{t('Umumiy narxi')}</Text>
<Text style={PaymentStyle.text}>{item.totalPrice}</Text>
</View>
</View>
</View>
</TouchableOpacity>
);
},
[handlePaymentPress, cardContainerStyle, badgeStyle, t],
);
return (
<View style={PaymentStyle.container}>
{packets?.data.map(renderPaymentCard)}
</View>
);
};
export default Payment;

View File

@@ -0,0 +1,268 @@
import { useQuery } from '@tanstack/react-query';
import packetsApi from 'api/packets';
import LoadingScreen from 'components/LoadingScreen';
import Navbar from 'components/Navbar';
import Navigation from 'components/Navigation';
import NoResult from 'components/NoResult';
import Pagination from 'components/Pagination';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
RefreshControl,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import Payment from './Payment';
const Wallet = () => {
const { t } = useTranslation();
const [refreshing, setRefreshing] = useState(false);
const [page, setPage] = useState(0);
const [pageAvia, setPageAvia] = useState(0);
const [selectedType, setSelectedType] = useState<'AVIA' | 'AUTO'>('AVIA');
const {
data: packets,
isFetching,
isLoading,
isError,
refetch,
} = useQuery({
queryKey: ['packets', page],
queryFn: () =>
packetsApi.getPackets({
page,
size: 10,
cargoType: 'AUTO',
direction: 'ASC',
sort: 'id',
}),
});
const {
data: packetsAvia,
isFetching: isFetchingAvia,
isLoading: isLoadingAvia,
isError: isErrorAvia,
refetch: refetchAvia,
} = useQuery({
queryKey: ['packetsAvia', pageAvia],
queryFn: () =>
packetsApi.getPackets({
page: pageAvia,
size: 10,
cargoType: 'AVIA',
direction: 'ASC',
sort: 'id',
}),
});
useEffect(() => {
const loadInitialData = async () => {
try {
await new Promise(resolve => setTimeout(resolve, 1000));
refetch();
refetchAvia();
} catch (error) {
console.error('Wallet loading error:', error);
refetch();
refetchAvia();
}
};
loadInitialData();
}, []);
const onRefresh = useCallback(async () => {
try {
setRefreshing(true);
await refetch();
} catch (error) {
console.error('Refresh error:', error);
} finally {
setRefreshing(false);
}
}, [refetch]);
const refreshControl = useMemo(
() => <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />,
[refreshing, onRefresh],
);
if (isLoading || isFetching || isLoadingAvia || isFetchingAvia) {
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={styles.container}>
<Navbar />
<LoadingScreen />
<Navigation />
</View>
</SafeAreaView>
);
}
if (isError || isErrorAvia) {
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={styles.container}>
<Navbar />
<NoResult message="Xatolik yuz berdi" />
<Navigation />
</View>
</SafeAreaView>
);
}
if (
(packets && !(packets?.data.length > 0)) ||
(packetsAvia && !(packetsAvia?.data.length > 0))
) {
return (
<SafeAreaView style={{ flex: 1 }}>
<Navbar />
<View style={styles.container}>
<View style={styles.header}>
<Text style={styles.title}>{t("To'lov")}</Text>
</View>
<NoResult />
</View>
<Navigation />
</SafeAreaView>
);
}
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={styles.container}>
<Navbar />
<ScrollView
refreshControl={refreshControl}
style={{ flex: 1 }}
contentContainerStyle={{ flexGrow: 1 }}
removeClippedSubviews={true}
keyboardShouldPersistTaps="handled"
>
<View style={{ flex: 1, justifyContent: 'space-between' }}>
<View>
<View style={styles.header}>
<Text style={styles.title}>{t("To'lov")}</Text>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<TouchableOpacity
style={{
padding: 5,
backgroundColor:
selectedType === 'AVIA' ? '#28A7E8' : '#F3FAFF',
borderTopLeftRadius: 8,
borderBottomLeftRadius: 8,
}}
onPress={() => setSelectedType('AVIA')}
>
<Text
style={{
color: selectedType === 'AVIA' ? '#fff' : '#28A7E8',
fontSize: 14,
}}
>
AVIA
</Text>
</TouchableOpacity>
<View style={{ width: 1 }} />
<TouchableOpacity
style={{
padding: 5,
backgroundColor:
selectedType === 'AUTO' ? '#28A7E8' : '#F3FAFF',
borderTopRightRadius: 8,
borderBottomRightRadius: 8,
}}
onPress={() => setSelectedType('AUTO')}
>
<Text
style={{
color: selectedType === 'AUTO' ? '#fff' : '#28A7E8',
fontSize: 14,
fontWeight: '500',
}}
>
AUTO
</Text>
</TouchableOpacity>
</View>
</View>
<Payment
packets={selectedType === 'AUTO' ? packets! : packetsAvia!}
/>
</View>
<View
style={{
flexDirection: 'row',
justifyContent: 'flex-end',
width: '95%',
alignSelf: 'center',
paddingRight: 20,
paddingVertical: 10,
}}
>
<Pagination
page={selectedType === 'AUTO' ? page : pageAvia}
totalPages={
selectedType === 'AUTO'
? packets?.totalPages ?? 1
: packetsAvia?.totalPages ?? 1
}
setPage={selectedType === 'AUTO' ? setPage : setPageAvia}
/>
</View>
</View>
</ScrollView>
<Navigation />
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
header: {
paddingHorizontal: 20,
paddingTop: 5,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#333',
},
cards: {
flexDirection: 'row',
justifyContent: 'space-around',
paddingHorizontal: 20,
paddingBottom: 10,
},
card: {
alignItems: 'center',
justifyContent: 'center',
width: 100,
height: 100,
backgroundColor: '#f0f0f0',
borderRadius: 10,
padding: 10,
},
cardText: {
marginTop: 5,
fontSize: 12,
color: '#555',
textAlign: 'center',
},
});
export default Wallet;

View File

@@ -0,0 +1,268 @@
import { StyleSheet } from 'react-native';
export const PaymentStyle = StyleSheet.create({
container: {
flex: 1,
width: '95%',
margin: 'auto',
marginTop: 5,
gap: 10,
},
card: {
padding: 20,
width: '95%',
backgroundColor: '#FFFFFF',
borderRadius: 8,
gap: 10,
},
cardHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
title: {
width: '60%',
fontSize: 18,
fontWeight: '600',
},
badge: {
backgroundColor: '#4CAF50',
color: '#fff',
fontSize: 12,
paddingHorizontal: 8,
paddingVertical: 2,
borderRadius: 5,
overflow: 'hidden',
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
},
infoTitle: {
fontWeight: '500',
fontSize: 14,
color: '#979797',
},
text: {
color: '#28A7E8',
fontWeight: '500',
fontSize: 16,
},
select: {
backgroundColor: '#FFFFFF',
height: 25,
width: 25,
borderWidth: 1,
borderRadius: 4,
alignItems: 'center',
justifyContent: 'center',
},
button: {
position: 'absolute',
bottom: 70,
left: 20,
right: 20,
height: 56,
borderRadius: 8,
justifyContent: 'center',
backgroundColor: '#28A7E8',
zIndex: 10,
},
modalBtn: {
position: 'absolute',
height: 56,
width: '100%',
borderRadius: 8,
justifyContent: 'center',
backgroundColor: '#28A7E8',
bottom: 10,
right: 20,
},
btnText: {
color: '#fff',
fontSize: 20,
textAlign: 'center',
},
containerMethod: {
width: '95%',
alignSelf: 'center',
marginTop: 20,
},
cardMethod: {
backgroundColor: '#FFFFFF',
borderRadius: 8,
},
planeContainer: {
alignItems: 'center',
marginBottom: 20,
},
planeIcon: {
position: 'absolute',
top: 10,
left: 85,
},
infoCard: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
paddingLeft: 20,
paddingRight: 20,
marginBottom: 20,
},
info: {
marginBottom: 15,
gap: 5,
},
titleMethod: {
color: '#1D1D1D',
fontSize: 18,
fontWeight: '500',
},
textMethod: {
color: '#555555',
fontSize: 16,
fontWeight: '400',
},
receiptCard: {
borderRadius: 10,
padding: 16,
marginBottom: 15,
gap: 8,
},
rowBetween: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
rowRight: {
flexDirection: 'row',
justifyContent: 'flex-end',
},
itemName: {
fontSize: 17,
fontWeight: '600',
color: '#333',
},
weight: {
fontSize: 16,
color: '#777',
},
track: {
fontSize: 15,
color: '#666',
},
price: {
fontSize: 15,
fontWeight: '500',
color: '#444',
},
total: {
fontSize: 16,
fontWeight: '600',
color: '#1D1D1D',
},
paymentCard: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
shadowOpacity: 0.3,
shadowRadius: 6,
shadowOffset: { width: 0, height: 2 },
},
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.4)',
justifyContent: 'center',
alignItems: 'center',
},
modalContent: {
width: '90%',
maxHeight: '80%',
backgroundColor: '#fff',
borderRadius: 10,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 4,
elevation: 5,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 20,
},
content: {
padding: 10,
margin: 'auto',
justifyContent: 'center',
alignItems: 'center',
gap: 10,
marginBottom: 20,
},
circle: {
backgroundColor: '#28A7E8',
width: 60,
height: 60,
borderRadius: 120,
justifyContent: 'center',
alignItems: 'center',
},
titleModal: {
fontSize: 16,
fontWeight: '500',
},
closeBtn: {
padding: 5,
width: 30,
height: 30,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#0000000D',
borderRadius: 50,
shadowColor: '#FFFFFF40',
shadowOpacity: 0.3,
shadowRadius: 6,
shadowOffset: { width: 0, height: 2 },
},
divider: {
width: '100%',
height: 1,
backgroundColor: '#E0E0E0',
marginBottom: 10,
marginHorizontal: 0,
},
status: {
color: '#00000099',
fontSize: 16,
fontWeight: '500',
},
label: {
fontSize: 12,
fontWeight: '500',
color: '#28A7E8',
marginBottom: 5,
},
errorText: {
color: 'red',
fontSize: 12,
},
input: {
borderWidth: 1,
borderColor: '#D8DADC',
borderRadius: 12,
padding: 15,
fontSize: 16,
fontWeight: '400',
backgroundColor: '#FFFFFF',
color: '#000000',
},
});

View File

@@ -0,0 +1,249 @@
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useMutation } from '@tanstack/react-query';
import packetsApi from 'api/packets';
import React, { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
ActivityIndicator,
Animated,
Dimensions,
Linking,
StyleSheet,
Text,
TouchableOpacity,
TouchableWithoutFeedback,
View,
} from 'react-native';
import Check from 'svg/Check';
import Click from 'svg/Click';
import Payme from 'svg/Payme';
import { RootStackParamList } from 'types/types';
import { PaymentStyle } from '../../payment/ui/style';
const { height } = Dimensions.get('window');
interface ModalCardViewProps {
isVisible: boolean;
setIsVisible: React.Dispatch<React.SetStateAction<boolean>>;
selectedId: 'click' | 'payme' | null;
packId: number;
setSelectedId: React.Dispatch<React.SetStateAction<'click' | 'payme' | null>>;
}
type NavigationProp = NativeStackNavigationProp<
RootStackParamList,
'PaymentMethod'
>;
const ModalCard = ({
isVisible,
setIsVisible,
packId,
selectedId,
setSelectedId,
}: ModalCardViewProps) => {
const slideAnim = useRef(new Animated.Value(height)).current;
const [load, setLoad] = React.useState(false);
const [pay, setPay] = useState<string>('');
const { t } = useTranslation();
const openLink = async (url: string) => {
try {
const supported = await Linking.canOpenURL(url);
if (supported) {
await Linking.openURL(url);
} else {
// Agar app ocholmasa, default brauzerda ochishga urinadi
await Linking.openURL(url);
}
} catch (err) {
console.error('Link xatolik:', err);
// Xato bolsa ham brauzer orqali ochishga urinish
try {
await Linking.openURL(url);
} catch (err2) {
console.error('Brauzer orqali ham ochilmadi:', err2);
}
}
};
const { mutate, isPending } = useMutation({
mutationFn: ({ id, payType }: { id: number; payType: string }) =>
packetsApi.payPackets(id, { payType }),
onSuccess: res => {
setIsVisible(false);
const url = res.data.paymentUrl;
openLink(url);
},
onError: err => {
console.dir(err);
},
});
const openModal = () => {
Animated.timing(slideAnim, {
toValue: 0,
duration: 100,
useNativeDriver: true,
}).start();
};
const closeModal = () => {
Animated.timing(slideAnim, {
toValue: height,
duration: 200,
useNativeDriver: true,
}).start(() => {
setIsVisible(false);
});
};
const handlePayment = () => {
mutate({ id: packId, payType: pay });
};
useEffect(() => {
if (isVisible) openModal();
}, [isVisible]);
if (!isVisible) return null;
return (
<View style={styles.overlay}>
<TouchableWithoutFeedback onPress={closeModal}>
<View style={styles.backdrop} />
</TouchableWithoutFeedback>
<Animated.View
style={[styles.sheet, { transform: [{ translateY: slideAnim }] }]}
>
<View style={styles.sheetContent}>
{/* CLICK */}
<TouchableOpacity
style={[
styles.paymentOption,
{
backgroundColor:
selectedId === 'click' ? '#28A7E81A' : '#FFFFFF',
},
]}
onPress={() => setSelectedId('click')}
>
<View style={PaymentStyle.paymentCard}>
<Click width={80} height={80} />
</View>
<View
style={[
PaymentStyle.select,
{
backgroundColor:
selectedId === 'click' ? '#28A7E8' : '#FFFFFF',
borderColor: selectedId === 'click' ? '#28A7E8' : '#383838',
},
]}
>
{selectedId === 'click' && (
<Check color="#fff" width={20} height={20} />
)}
</View>
</TouchableOpacity>
{/* PAYME */}
<TouchableOpacity
style={[
styles.paymentOption,
{ backgroundColor: '#FFFFFF', marginBottom: 50 },
]}
onPress={() => {
setPay('PAYME'), setSelectedId('payme');
}}
>
<View style={PaymentStyle.paymentCard}>
<Payme width={80} height={80} />
</View>
<View
style={[
PaymentStyle.select,
{
backgroundColor:
selectedId === 'payme' ? '#28A7E8' : '#FFFFFF',
borderColor: selectedId === 'payme' ? '#28A7E8' : '#383838',
},
]}
>
{selectedId === 'payme' && (
<Check color="#fff" width={20} height={20} />
)}
</View>
</TouchableOpacity>
<TouchableOpacity
style={[
PaymentStyle.modalBtn,
{
margin: 'auto',
right: 0,
bottom: -20,
backgroundColor:
load || selectedId === null ? '#88888840' : '#28A7E8',
},
]}
onPress={handlePayment}
disabled={load}
>
{load || isPending ? (
<ActivityIndicator size="small" color="#fff" />
) : (
<Text
style={[
PaymentStyle.btnText,
{
color: load || selectedId === null ? '#000000b9' : '#FFFF',
},
]}
>
{t("To'lash")}
</Text>
)}
</TouchableOpacity>
</View>
</Animated.View>
</View>
);
};
const styles = StyleSheet.create({
overlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: 1000,
justifyContent: 'flex-end',
},
backdrop: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0,0,0,0.4)',
},
sheet: {
width: '100%',
backgroundColor: '#F3FAFF',
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
padding: 20,
paddingBottom: 30,
},
sheetContent: {
gap: 10,
},
paymentOption: {
flexDirection: 'row',
justifyContent: 'space-between',
borderRadius: 10,
paddingLeft: 20,
paddingRight: 20,
alignItems: 'center',
},
});
export default ModalCard;

View File

@@ -0,0 +1,261 @@
import React, { useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import {
ActivityIndicator,
Animated,
Dimensions,
StyleSheet,
Text,
TouchableOpacity,
TouchableWithoutFeedback,
View,
} from 'react-native';
import Check from 'svg/Check';
import CreditCard from 'svg/CreditCard';
import Usd from 'svg/Dollar';
import { PaymentStyle } from '../../payment/ui/style';
const SCREEN_HEIGHT = Dimensions.get('window').height;
interface ModalPayProps {
isModalVisible: boolean;
setModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
selectedId: 'card' | 'pay' | null;
setSelectedId: React.Dispatch<React.SetStateAction<'card' | 'pay' | null>>;
cardModal: boolean;
setCardModal: React.Dispatch<React.SetStateAction<boolean>>;
payModal: boolean;
setPayModal: React.Dispatch<React.SetStateAction<boolean>>;
success: boolean;
setSuccess: React.Dispatch<React.SetStateAction<boolean>>;
}
const ModalPay = ({
isModalVisible,
setModalVisible,
selectedId,
setSelectedId,
setCardModal,
setPayModal,
}: ModalPayProps) => {
const translateY = useRef(new Animated.Value(SCREEN_HEIGHT)).current;
const opacity = useRef(new Animated.Value(0)).current;
const [load, setLoad] = React.useState(false);
const { t } = useTranslation();
useEffect(() => {
if (isModalVisible) {
Animated.parallel([
Animated.timing(translateY, {
toValue: 0,
duration: 300,
useNativeDriver: true,
}),
Animated.timing(opacity, {
toValue: 1,
duration: 300,
useNativeDriver: true,
}),
]).start();
} else {
Animated.parallel([
Animated.timing(translateY, {
toValue: SCREEN_HEIGHT,
duration: 300,
useNativeDriver: true,
}),
Animated.timing(opacity, {
toValue: 0,
duration: 200,
useNativeDriver: true,
}),
]).start();
}
}, [isModalVisible]);
const handleBackdropPress = () => {
setModalVisible(false);
setSelectedId(null);
};
const handlePayment = () => {
setLoad(true);
setTimeout(() => {
if (selectedId === 'pay') setPayModal(true);
if (selectedId === 'card') setCardModal(true);
setModalVisible(false);
setSelectedId(null);
setLoad(false);
}, 200);
};
if (!isModalVisible) return null;
return (
<TouchableWithoutFeedback onPress={handleBackdropPress}>
<Animated.View
style={[
styles.overlay,
{
opacity: opacity,
},
]}
>
<TouchableWithoutFeedback>
<Animated.View
style={[
styles.modalContent,
{
transform: [{ translateY: translateY }],
},
]}
>
{/* CARD OPTION */}
<TouchableOpacity
style={[
styles.option,
{
backgroundColor: selectedId === 'card' ? '#28A7E81A' : '#fff',
},
]}
onPress={() => setSelectedId('card')}
>
<View style={PaymentStyle.paymentCard}>
<CreditCard
color={selectedId == 'card' ? '#28A7E8' : '#000000'}
width={28}
height={28}
/>
<Text
style={[
PaymentStyle.titleMethod,
{ color: selectedId == 'card' ? '#28A7E8' : '#000' },
]}
>
{t('Bank kartasi')}
</Text>
</View>
<View
style={[
PaymentStyle.select,
{
backgroundColor:
selectedId === 'card' ? '#28A7E8' : '#FFFFFF',
borderColor: selectedId === 'card' ? '#28A7E8' : '#383838',
},
]}
>
{selectedId === 'card' && (
<Check color="#fff" width={20} height={20} />
)}
</View>
</TouchableOpacity>
{/* CASH OPTION */}
<TouchableOpacity
style={[
styles.option,
{
backgroundColor: selectedId === 'pay' ? '#28A7E81A' : '#fff',
},
]}
onPress={() => setSelectedId('pay')}
>
<View style={PaymentStyle.paymentCard}>
<Usd
color={selectedId == 'pay' ? '#28A7E8' : '#000000'}
width={28}
height={28}
colorCircle={selectedId == 'pay' ? '#28A7E8' : '#000000'}
/>
<Text
style={[
PaymentStyle.titleMethod,
{ color: selectedId == 'pay' ? '#28A7E8' : '#000' },
]}
>
{t('Naqt pul')}
</Text>
</View>
<View
style={[
PaymentStyle.select,
{
backgroundColor:
selectedId === 'pay' ? '#28A7E8' : '#FFFFFF',
borderColor: selectedId === 'pay' ? '#28A7E8' : '#383838',
},
]}
>
{selectedId === 'pay' && (
<Check color="#fff" width={20} height={20} />
)}
</View>
</TouchableOpacity>
{/* BUTTON */}
<TouchableOpacity
style={[
PaymentStyle.modalBtn,
{
backgroundColor:
load || selectedId === null ? '#88888840' : '#28A7E8',
},
]}
onPress={handlePayment}
disabled={load || selectedId === null}
>
{load ? (
<ActivityIndicator size="small" color="#fff" />
) : (
<Text
style={[
PaymentStyle.btnText,
{
color:
load || selectedId === null ? '#000000b9' : '#FFFF',
},
]}
>
{t("To'lash")}
</Text>
)}
</TouchableOpacity>
</Animated.View>
</TouchableWithoutFeedback>
</Animated.View>
</TouchableWithoutFeedback>
);
};
export default ModalPay;
const styles = StyleSheet.create({
overlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0,0,0,0.4)',
justifyContent: 'flex-end',
zIndex: 30,
},
modalContent: {
backgroundColor: '#F3FAFF',
padding: 20,
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
minHeight: 250,
gap: 10,
},
option: {
flexDirection: 'row',
justifyContent: 'space-between',
borderRadius: 10,
gap: 10,
alignItems: 'center',
padding: 20,
},
});

View File

@@ -0,0 +1,246 @@
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import LottieView from 'lottie-react-native';
import React, { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Animated,
StyleSheet,
Text,
TouchableOpacity,
useWindowDimensions,
View,
} from 'react-native';
import Clock from 'screens/../../assets/lottie/Sand clock.json';
import ProgressBar from 'screens/../../assets/lottie/Success.json';
import Warning from 'screens/../../assets/lottie/Warning animation.json';
import CloseIcon from 'svg/Close';
import { RootStackParamList } from 'types/types';
type NavigationProp = NativeStackNavigationProp<
RootStackParamList,
'PaymentMethod'
>;
interface ModalSuccessViewProps {
visible: boolean;
setVisible: React.Dispatch<React.SetStateAction<boolean>>;
setPayModal: React.Dispatch<React.SetStateAction<boolean>>;
}
const ModalSuccess = ({
visible,
setVisible,
setPayModal,
}: ModalSuccessViewProps) => {
const navigation = useNavigation<NavigationProp>();
const [successMet, setSuccessMet] = useState(false);
const [error, setError] = useState(false);
const opacity = useRef(new Animated.Value(0)).current;
const translateY = useRef(new Animated.Value(50)).current;
const { t } = useTranslation();
const { width } = useWindowDimensions();
const scale = width < 360 ? 0.8 : 1;
const closeModal = () => {
Animated.timing(opacity, {
toValue: 0,
duration: 200,
useNativeDriver: true,
}).start(() => {
setVisible(false);
setPayModal(false);
navigation.navigate('Wallet');
});
};
useEffect(() => {
if (visible) {
Animated.parallel([
Animated.timing(opacity, {
toValue: 1,
duration: 250,
useNativeDriver: true,
}),
Animated.spring(translateY, {
toValue: 0,
useNativeDriver: true,
}),
]).start();
const timer = setTimeout(() => setSuccessMet(true), 3000);
return () => clearTimeout(timer);
} else {
setSuccessMet(false);
}
}, [visible]);
if (!visible) return null;
return (
<View style={styles.overlay}>
<Animated.View
style={[
styles.modalContent,
{
opacity: opacity,
transform: [{ translateY }],
},
]}
>
<View style={styles.header}>
<Text style={styles.title}>
{successMet
? t('Tolov muvaffaqqiyatli otdi')
: t('Chop etilmoqda')}
</Text>
<TouchableOpacity
onPress={closeModal}
style={styles.closeBtn}
disabled={!successMet}
>
<CloseIcon width={15} height={15} color={'#000000'} />
</TouchableOpacity>
</View>
<View style={styles.divider} />
<View style={styles.content}>
{successMet ? (
<View style={styles.content}>
{error ? (
<LottieView
source={Warning}
loop
autoPlay={true}
resizeMode="cover"
style={{ width: 100 * scale, height: 100 * scale }}
/>
) : (
<LottieView
source={ProgressBar}
loop
autoPlay={true}
resizeMode="cover"
style={{ width: 100 * scale, height: 100 * scale }}
/>
)}
</View>
) : (
<LottieView
source={Clock}
loop
autoPlay={true}
resizeMode="cover"
style={{ width: 100 * scale, height: 100 * scale }}
/>
)}
<Text style={styles.status}>
{successMet
? t('Toʻlovingiz tasdiqlandi!')
: t('Iltimos ozroq kutib turing!')}
</Text>
</View>
{successMet && (
<>
{error ? (
<TouchableOpacity
style={styles.btn}
onPress={() => setPayModal(false)}
>
<Text style={styles.btnText}>{t('Yaxshi')}</Text>
</TouchableOpacity>
) : (
<TouchableOpacity style={styles.btn} onPress={closeModal}>
<Text style={styles.btnText}>{t('Yaxshi')}</Text>
</TouchableOpacity>
)}
</>
)}
</Animated.View>
</View>
);
};
const styles = StyleSheet.create({
overlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0,0,0,0.4)',
justifyContent: 'center',
alignItems: 'center',
zIndex: 1000,
},
modalContent: {
width: '90%',
backgroundColor: '#fff',
borderRadius: 10,
paddingBottom: 20,
shadowColor: '#000',
shadowOpacity: 0.3,
shadowRadius: 4,
elevation: 6,
},
header: {
padding: 20,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
title: {
fontSize: 16,
fontWeight: '500',
},
closeBtn: {
padding: 5,
width: 30,
height: 30,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#0000000D',
borderRadius: 50,
},
divider: {
height: 1,
backgroundColor: '#E0E0E0',
marginBottom: 10,
},
content: {
alignItems: 'center',
gap: 10,
paddingVertical: 20,
},
circle: {
backgroundColor: '#28A7E8',
width: 60,
height: 60,
borderRadius: 30,
justifyContent: 'center',
alignItems: 'center',
},
status: {
color: '#00000099',
fontSize: 16,
fontWeight: '500',
},
btn: {
alignSelf: 'center',
height: 50,
width: '90%',
borderRadius: 8,
justifyContent: 'center',
backgroundColor: '#28A7E8',
marginTop: 10,
},
btnText: {
textAlign: 'center',
color: '#fff',
fontSize: 18,
},
});
export default ModalSuccess;

View File

@@ -0,0 +1,81 @@
import { RouteProp, useRoute } from '@react-navigation/native';
import NavbarBack from 'components/NavbarBack';
import Navigation from 'components/Navigation';
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import { Text, TouchableOpacity, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { PaymentStyle } from '../../payment/ui/style';
import ModalCard from './ModalCard';
import ModalPay from './ModalPay';
import ModalSuccess from './ModalSuccess';
import PaymentProduct from './PaymentProduct';
const PaymentMethod = () => {
const route = useRoute<RouteProp<any, 'PaymentMethod'>>();
const packets = route.params?.packets;
const [isModalVisible, setModalVisible] = React.useState(false);
const { t } = useTranslation();
const [selectedId, setSelectedId] = React.useState<'card' | 'pay' | null>(
null,
);
const [selectedCard, setSelectedCard] = React.useState<
'click' | 'payme' | null
>(null);
const [cardModal, setCardModal] = React.useState(false);
const [payModal, setPayModal] = React.useState(false);
const [success, setSuccess] = React.useState(false);
const toggleModal = () => setModalVisible(true);
React.useEffect(() => {
if (payModal) {
const timeout = setTimeout(() => setSuccess(true), 1000);
return () => clearTimeout(timeout);
}
}, [payModal]);
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={{ flex: 1 }}>
<NavbarBack title={t("To'lov usuli")} />
<PaymentProduct packet={packets!} />
<ModalPay
isModalVisible={isModalVisible}
selectedId={selectedId}
setModalVisible={setModalVisible}
setSelectedId={setSelectedId}
cardModal={cardModal}
setCardModal={setCardModal}
payModal={payModal}
setPayModal={setPayModal}
success={success}
setSuccess={setSuccess}
/>
{cardModal && (
<ModalCard
isVisible={cardModal}
packId={packets.id}
setIsVisible={setCardModal}
selectedId={selectedCard}
setSelectedId={setSelectedCard}
/>
)}
{success && (
<ModalSuccess
visible={success}
setVisible={setSuccess}
setPayModal={setPayModal}
/>
)}
<TouchableOpacity style={[PaymentStyle.button]} onPress={toggleModal}>
<Text style={PaymentStyle.btnText}>{t("To'lash")}</Text>
</TouchableOpacity>
<Navigation />
</View>
</SafeAreaView>
);
};
export default PaymentMethod;

Some files were not shown because too many files have changed in this diff Show More