Initial commit
This commit is contained in:
13
src/api/URL.ts
Normal file
13
src/api/URL.ts
Normal 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
50
src/api/auth/index.ts
Normal 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
38
src/api/auth/type.ts
Normal 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
40
src/api/axios.ts
Normal 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}`;
|
||||
}
|
||||
|
||||
// Language’ni 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
28
src/api/branch/index.ts
Normal 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
33
src/api/calendar/index.ts
Normal 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
60
src/api/packets/index.ts
Normal 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
50
src/api/passport/index.ts
Normal 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;
|
||||
66
src/components/AnimatedDots.tsx
Normal file
66
src/components/AnimatedDots.tsx
Normal 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;
|
||||
150
src/components/AnimatedIcon.tsx
Normal file
150
src/components/AnimatedIcon.tsx
Normal 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;
|
||||
71
src/components/AnimatedScreen.tsx
Normal file
71
src/components/AnimatedScreen.tsx
Normal 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;
|
||||
183
src/components/BottomModal.tsx
Normal file
183
src/components/BottomModal.tsx
Normal 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',
|
||||
},
|
||||
});
|
||||
58
src/components/CustomAlertModal.tsx
Normal file
58
src/components/CustomAlertModal.tsx
Normal 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,
|
||||
},
|
||||
});
|
||||
141
src/components/DatePicker.tsx
Normal file
141
src/components/DatePicker.tsx
Normal 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
224
src/components/FileDrop.tsx
Normal 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;
|
||||
89
src/components/GlobalModal.tsx
Normal file
89
src/components/GlobalModal.tsx
Normal 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;
|
||||
43
src/components/InAppBrowser.tsx
Normal file
43
src/components/InAppBrowser.tsx
Normal 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;
|
||||
64
src/components/LoadingScreen.tsx
Normal file
64
src/components/LoadingScreen.tsx
Normal 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; // ✅ Qo‘shimcha 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
175
src/components/Navbar.tsx
Normal 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, store’ni ham ochib bo‘lmasa, 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, store’ni ham ochib bo‘lmasa, 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;
|
||||
40
src/components/NavbarBack.tsx
Normal file
40
src/components/NavbarBack.tsx
Normal 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,
|
||||
},
|
||||
});
|
||||
119
src/components/Navigation.tsx
Normal file
119
src/components/Navigation.tsx
Normal 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;
|
||||
24
src/components/NavigationRef.ts
Normal file
24
src/components/NavigationRef.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
75
src/components/NoResult.tsx
Normal file
75
src/components/NoResult.tsx
Normal 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;
|
||||
131
src/components/Pagination.tsx
Normal file
131
src/components/Pagination.tsx
Normal 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 bo‘lsa ... qo‘shamiz
|
||||
if (page > 3) {
|
||||
pages.push('...');
|
||||
}
|
||||
|
||||
// current page atrofidagi 2 ta sahifani ko‘rsatamiz
|
||||
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 bo‘lmasa, ... qo‘shamiz
|
||||
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;
|
||||
38
src/components/PatternPad.tsx
Normal file
38
src/components/PatternPad.tsx
Normal 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',
|
||||
},
|
||||
});
|
||||
201
src/components/SplashScreen.tsx
Normal file
201
src/components/SplashScreen.tsx
Normal 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
10
src/helpers/formatData.ts
Normal 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;
|
||||
38
src/helpers/formatPhone.ts
Normal file
38
src/helpers/formatPhone.ts
Normal 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;
|
||||
7
src/helpers/getWeekday.ts
Normal file
7
src/helpers/getWeekday.ts
Normal 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
40
src/i18n/i18n.ts
Normal 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
4
src/i18n/locales/en.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"hello": "Hello",
|
||||
"select_language": "Select Language"
|
||||
}
|
||||
229
src/i18n/locales/ru.json
Normal file
229
src/i18n/locales/ru.json
Normal file
@@ -0,0 +1,229 @@
|
||||
{
|
||||
"hello": "Здравствуйте",
|
||||
"select_language": "Выберите язык",
|
||||
"Ro’yxatdan o’tganmisz": "Вы зарегистрированы?",
|
||||
"Botdan ro'yxatdan o’tganmisiz": "Вы зарегистрированы через бота?",
|
||||
"Tizimga kirish": "Войти в систему",
|
||||
"Yangi ro’yxatdan o'tmoqchimisiz": "Хотите зарегистрироваться?",
|
||||
"Ro’yxatdan o’tish": "Регистрация",
|
||||
"Telefon raqami": "Номер телефона",
|
||||
"Passport seriya raqami": "Серия и номер паспорта",
|
||||
"Filial": "Филиал",
|
||||
"Filialni tanlang...": "Выберите филиал...",
|
||||
"ID va kabinet yo’qmi?": "Нет 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. Doimiyko’p 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 ro’yxati": "Список филиалов",
|
||||
"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 ko‘p yuk oluvchilar uchun maxsus chegirmali narxlarimiz mavjud.": "В авто-карго можно приобрести один и тот же товар серийно, в любом большом количестве. Для постоянных крупных заказчиков у нас есть специальные скидки.",
|
||||
"dan boshlab": "от",
|
||||
"Aviada taqiqlangan buyumlar": "Запрещенные товары для авиа-доставки",
|
||||
"Ichida suyuqligi bor narsalar": "Предметы с жидкостями внутри",
|
||||
"Batareykasi va magnit bo’lgan istalgan narsa": "Любые предметы с батареей и магнитом",
|
||||
"(Telifon, sensitive buyumlar, airpods, naushnik, qo’l soati, tagi yonadigan krasovkalar...)": "(Телефоны, чувствительные устройства, airpods, наушники, наручные часы, кроссовки с подсветкой...)",
|
||||
"Kukunli buyumlar": "Порошкообразные вещества",
|
||||
"(Pudra, ten...)": "(Пудра, тени...)",
|
||||
"Parfumeriya": "Парфюмерия",
|
||||
"(Barcha Parfumeriya va kosmetika, yuvinish maxsulotlari)": "(Вся парфюмерия и косметика, средства для мытья)",
|
||||
"O’tkir tig’li 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 o’sishiga 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 og’irligi": "Вес товаров",
|
||||
"Umumiy narxi": "Общая цена",
|
||||
"Buyurtmalar soni": "Количество заказов",
|
||||
"Mahsulotlar": "Продукты",
|
||||
"Og’irligi": "Вес",
|
||||
"Trek ID": "Трек ID",
|
||||
"Narxi": "Цена",
|
||||
"so‘m": "сум",
|
||||
"Umumiy narx": "Общая цена",
|
||||
"Yopish": "Закрыть",
|
||||
"Passportlarim": "Мои паспорта",
|
||||
"Hali pasport qo'shilmagan": "Паспорт еще не добавлен",
|
||||
"Yangi pasport qo'shish uchun tugmani bosing": "Нажмите кнопку, чтобы добавить новый паспорт",
|
||||
"Yangi pasport qo'shish": "Добавить новый паспорт",
|
||||
"Passport ma’lumotlarim": "Мои паспортные данные",
|
||||
"Tez ID": "Быстрый ID",
|
||||
"To’liq ismi": "Полное имя",
|
||||
"Passport seriya": "Серия паспорта",
|
||||
"Tug’ilgan 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": "Наличные",
|
||||
"To’lov muvaffaqqiyatli o’tdi": "Платёж прошёл успешно",
|
||||
"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 o‘chirish": "Удалить аккаунт",
|
||||
"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
230
src/i18n/locales/uz.json
Normal file
@@ -0,0 +1,230 @@
|
||||
{
|
||||
"hello": "Salom",
|
||||
"select_language": "Tilni tanlang",
|
||||
"Ro’yxatdan o’tganmisz": "Ro’yxatdan o’tganmisz",
|
||||
"Botdan ro'yxatdan o’tganmisiz": "Botdan ro'yxatdan o’tganmisiz",
|
||||
"Tizimga kirish": "Tizimga kirish",
|
||||
"Yangi ro’yxatdan o'tmoqchimisiz": "Yangi ro’yxatdan o'tmoqchimisiz",
|
||||
"Ro’yxatdan o’tish": "Ro’yxatdan o’tish",
|
||||
"Telefon raqami": "Telefon raqami",
|
||||
"Passport seriya raqami": "Passport seriya raqami",
|
||||
"Filial": "Filial",
|
||||
"Filialni tanlang...": "Filialni tanlang...",
|
||||
"ID va kabinet yo’qmi?": "ID va kabinet yo’qmi?",
|
||||
"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 ro’yxati": "Filiallar ro’yxati",
|
||||
"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 o’sishiga olib keladi. Seriyali buyularni avto kargo orqali olib kelish arzonga tushadi)": "(Bu kargo narxini o’sishiga 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 ko‘p yuk oluvchilar uchun maxsus chegirmali narxlarimiz mavjud.": "Avto kargoda bir mahsulotdan seriyali ravishda, istalgan katta miqdorda xarid qilish mumkin. Doimiy ravishda ko‘p 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 bo’lgan istalgan narsa": "Batareykasi va magnit bo’lgan istalgan narsa",
|
||||
"(Telifon, sensitive buyumlar, airpods, naushnik, qo’l soati, tagi yonadigan krasovkalar...)": "(Telifon, sensitive buyumlar, airpods, naushnik, qo’l 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)",
|
||||
"O’tkir tig’li va sovuq qirollar": "O’tkir tig’li 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 og’irligi": "Mahsulotlar og’irligi",
|
||||
"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. Doimiyko’p yuk oluvchi ijozlar uchun maxsus arzonlashtrilgan narxlarimiz bor": "Avto kargoda bir maxsulotdan seriyalab istalgan katta miqdorda olish mumkin. Doimiyko’p 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А",
|
||||
"Og’irligi": "Og’irligi",
|
||||
"IN_WAREHOUSE": "OMBORDA",
|
||||
"Trek ID": "Trek ID",
|
||||
"Hech qanday ma'lumot topilmadi": "Hech qanday ma'lumot topilmadi",
|
||||
"Narxi": "Narxi",
|
||||
"Yetkazish tafsilotlari": "Yetkazish tafsilotlari",
|
||||
"so‘m": "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 ma’lumotlarim": "Passport ma’lumotlarim",
|
||||
"Tez ID": "Tez ID",
|
||||
"To’liq ismi": "To’liq ismi",
|
||||
"Passport seriya": "Passport seriya",
|
||||
"Tug’ilgan kun": "Tug’ilgan 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",
|
||||
"To’lov muvaffaqqiyatli o’tdi": "To’lov muvaffaqqiyatli o’tdi",
|
||||
"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 o‘chirish": "Hisobingizni o‘chirish",
|
||||
"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"
|
||||
}
|
||||
11
src/screens/auth/login/lib/form.ts
Normal file
11
src/screens/auth/login/lib/form.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
// form.ts
|
||||
import { z } from 'zod';
|
||||
|
||||
export const loginSchema = z.object({
|
||||
phone: z.string().min(12, 'Xato raqam kiritildi'),
|
||||
passportSeriya: z.string().length(2, '2 ta harf kerak'),
|
||||
passportNumber: z.string().length(7, '7 ta raqam kerak'),
|
||||
branchId: z.number().min(1, 'Filialni tanlang'),
|
||||
});
|
||||
|
||||
export type LoginFormType = z.infer<typeof loginSchema>;
|
||||
32
src/screens/auth/login/lib/userstore.ts
Normal file
32
src/screens/auth/login/lib/userstore.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface UserState {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
phoneNumber: string;
|
||||
expireTime: number;
|
||||
setExpireTime: (time: number) => void;
|
||||
setUser: (user: Partial<UserState>) => void;
|
||||
clearUser: () => void;
|
||||
}
|
||||
|
||||
export const useUserStore = create<UserState>(set => ({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
phoneNumber: '',
|
||||
expireTime: 0,
|
||||
setExpireTime: time => set({ expireTime: time }),
|
||||
|
||||
setUser: user =>
|
||||
set(state => ({
|
||||
...state,
|
||||
...user,
|
||||
})),
|
||||
|
||||
clearUser: () =>
|
||||
set({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
phoneNumber: '',
|
||||
}),
|
||||
}));
|
||||
321
src/screens/auth/login/ui/Confirm.tsx
Normal file
321
src/screens/auth/login/ui/Confirm.tsx
Normal file
@@ -0,0 +1,321 @@
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { authApi } from 'api/auth';
|
||||
import { otpPayload, resendPayload } from 'api/auth/type';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
ImageBackground,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import SimpleLineIcons from 'react-native-vector-icons/SimpleLineIcons';
|
||||
import Logo from 'screens/../../assets/bootsplash/logo_512.png';
|
||||
import { useModalStore } from 'screens/auth/registeration/lib/modalStore';
|
||||
import LanguageSelector from 'screens/auth/select-language/SelectLang';
|
||||
import { RootStackParamList } from 'types/types';
|
||||
import { useUserStore } from '../lib/userstore';
|
||||
import { Loginstyle } from './styled';
|
||||
|
||||
type VerificationCodeScreenNavigationProp = NativeStackNavigationProp<
|
||||
RootStackParamList,
|
||||
'Confirm'
|
||||
>;
|
||||
|
||||
const OTP_LENGTH = 4;
|
||||
|
||||
const Confirm = () => {
|
||||
const navigation = useNavigation<VerificationCodeScreenNavigationProp>();
|
||||
const { t } = useTranslation();
|
||||
const [code, setCode] = useState<string[]>(new Array(OTP_LENGTH).fill(''));
|
||||
const [timer, setTimer] = useState(60);
|
||||
const [errorConfirm, setErrorConfirm] = useState<string | null>(null);
|
||||
const [canResend, setCanResend] = useState(false);
|
||||
const inputRefs = useRef<Array<TextInput | null>>([]);
|
||||
const { phoneNumber } = useUserStore(state => state);
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: (payload: otpPayload) => authApi.verifyOtp(payload),
|
||||
onSuccess: async res => {
|
||||
await AsyncStorage.setItem('token', res.data.accessToken);
|
||||
navigation.navigate('Home');
|
||||
setErrorConfirm(null);
|
||||
},
|
||||
onError: (err: any) => {
|
||||
setErrorConfirm(err?.response.data.message);
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: resendMutate } = useMutation({
|
||||
mutationFn: (payload: resendPayload) => authApi.resendOtp(payload),
|
||||
onSuccess: async res => {
|
||||
setTimer(60);
|
||||
setCanResend(false);
|
||||
setCode(new Array(OTP_LENGTH).fill(''));
|
||||
inputRefs.current[0]?.focus();
|
||||
setErrorConfirm(null);
|
||||
},
|
||||
onError: (err: any) => {
|
||||
setErrorConfirm(err?.response.data.message);
|
||||
},
|
||||
});
|
||||
|
||||
const openModal = useModalStore(state => state.openModal);
|
||||
useEffect(() => {
|
||||
let interval: NodeJS.Timeout | null = null;
|
||||
if (timer > 0) {
|
||||
interval = setInterval(() => {
|
||||
setTimer(prevTimer => prevTimer - 1);
|
||||
}, 1000);
|
||||
} else {
|
||||
setCanResend(true);
|
||||
if (interval) clearInterval(interval);
|
||||
}
|
||||
return () => {
|
||||
if (interval) clearInterval(interval);
|
||||
};
|
||||
}, [timer]);
|
||||
|
||||
const handleCodeChange = (text: string, index: number) => {
|
||||
const newCode = [...code];
|
||||
newCode[index] = text;
|
||||
setCode(newCode);
|
||||
|
||||
if (text.length > 0 && index < OTP_LENGTH - 1) {
|
||||
inputRefs.current[index + 1]?.focus();
|
||||
}
|
||||
if (text.length === 0 && index > 0) {
|
||||
inputRefs.current[index - 1]?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyPress = ({ nativeEvent }: any, index: number) => {
|
||||
if (nativeEvent.key === 'Backspace' && code[index] === '' && index > 0) {
|
||||
inputRefs.current[index - 1]?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const handleResendCode = () => {
|
||||
resendMutate({
|
||||
phoneNumber: phoneNumber,
|
||||
otpType: 'LOGIN',
|
||||
});
|
||||
};
|
||||
|
||||
const handleVerifyCode = () => {
|
||||
const enteredCode = code.join('');
|
||||
mutate({
|
||||
phoneNumber: phoneNumber,
|
||||
otp: String(enteredCode),
|
||||
otpType: 'LOGIN',
|
||||
});
|
||||
// navigation.navigate('Home');
|
||||
};
|
||||
|
||||
return (
|
||||
<ImageBackground
|
||||
source={Logo}
|
||||
style={Loginstyle.background}
|
||||
resizeMode="contain"
|
||||
imageStyle={{
|
||||
opacity: 0.3,
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
transform: [{ scale: 1.5 }],
|
||||
}}
|
||||
>
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<View style={styles.langContainer}>
|
||||
<TouchableOpacity onPress={() => navigation.navigate('Login')}>
|
||||
<SimpleLineIcons name="arrow-left" color="#000" size={20} />
|
||||
</TouchableOpacity>
|
||||
<LanguageSelector />
|
||||
</View>
|
||||
<KeyboardAvoidingView
|
||||
style={styles.container}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
>
|
||||
<View style={styles.content}>
|
||||
<Text style={styles.title}>{t('Tasdiqlash kodini kiriting')}</Text>
|
||||
<Text style={styles.message}>
|
||||
{phoneNumber} {t('raqamiga yuborilgan')} {OTP_LENGTH}{' '}
|
||||
{t('xonali kodni kiriting.')}
|
||||
</Text>
|
||||
<View style={styles.otpContainer}>
|
||||
{code.map((digit, index) => (
|
||||
<TextInput
|
||||
key={index}
|
||||
ref={ref => {
|
||||
inputRefs.current[index] = ref;
|
||||
}}
|
||||
style={styles.otpInput}
|
||||
keyboardType="number-pad"
|
||||
maxLength={1}
|
||||
onChangeText={text => handleCodeChange(text, index)}
|
||||
onKeyPress={e => handleKeyPress(e, index)}
|
||||
value={digit}
|
||||
autoFocus={index === 0}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
{errorConfirm !== null && (
|
||||
<Text style={styles.errorText}>{errorConfirm}</Text>
|
||||
)}
|
||||
<View style={styles.resendContainer}>
|
||||
{canResend ? (
|
||||
<TouchableOpacity
|
||||
onPress={handleResendCode}
|
||||
style={styles.resendButton}
|
||||
>
|
||||
<Text style={styles.resendButtonText}>
|
||||
{t('Kodni qayta yuborish')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<Text style={styles.timerText}>
|
||||
{t('Kodni qayta yuborish vaqti')} ({timer}s)
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={styles.verifyButton}
|
||||
onPress={handleVerifyCode}
|
||||
>
|
||||
{isPending ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<Text
|
||||
style={[
|
||||
Loginstyle.btnText,
|
||||
isPending && styles.buttonTextDisabled,
|
||||
]}
|
||||
>
|
||||
{t('Tasdiqlash')}
|
||||
</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
</ImageBackground>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safeArea: {
|
||||
flex: 1,
|
||||
backgroundColor: '#f8f9fa',
|
||||
},
|
||||
errorText: {
|
||||
color: 'red',
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
marginTop: 10,
|
||||
textAlign: 'center',
|
||||
},
|
||||
buttonTextDisabled: {
|
||||
color: 'white',
|
||||
},
|
||||
langContainer: {
|
||||
width: '100%',
|
||||
marginTop: 10,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
color: '#333',
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 20,
|
||||
},
|
||||
content: {
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
color: '#333',
|
||||
marginBottom: 10,
|
||||
textAlign: 'center',
|
||||
},
|
||||
message: {
|
||||
fontSize: 15,
|
||||
color: '#666',
|
||||
textAlign: 'center',
|
||||
marginBottom: 30,
|
||||
lineHeight: 22,
|
||||
},
|
||||
otpContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
},
|
||||
otpInput: {
|
||||
width: 45,
|
||||
height: 55,
|
||||
borderWidth: 1,
|
||||
borderColor: '#ccc',
|
||||
borderRadius: 8,
|
||||
textAlign: 'center',
|
||||
fontSize: 22,
|
||||
fontWeight: 'bold',
|
||||
color: '#333',
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
resendContainer: {
|
||||
marginBottom: 30,
|
||||
marginTop: 20,
|
||||
},
|
||||
timerText: {
|
||||
fontSize: 15,
|
||||
color: '#999',
|
||||
},
|
||||
resendButton: {
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 20,
|
||||
borderRadius: 8,
|
||||
},
|
||||
resendButtonText: {
|
||||
fontSize: 15,
|
||||
color: '#007bff',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
verifyButton: {
|
||||
backgroundColor: '#007bff',
|
||||
paddingVertical: 15,
|
||||
paddingHorizontal: 40,
|
||||
borderRadius: 8,
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
elevation: 3,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 4,
|
||||
},
|
||||
verifyButtonText: {
|
||||
color: '#fff',
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
||||
export default Confirm;
|
||||
327
src/screens/auth/login/ui/index.tsx
Normal file
327
src/screens/auth/login/ui/index.tsx
Normal file
@@ -0,0 +1,327 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { authApi } from 'api/auth';
|
||||
import { loginPayload } from 'api/auth/type';
|
||||
import { Branch, branchApi } from 'api/branch';
|
||||
import formatPhone from 'helpers/formatPhone';
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
ImageBackground,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
ScrollView,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import SimpleLineIcons from 'react-native-vector-icons/SimpleLineIcons';
|
||||
import Logo from 'screens/../../assets/bootsplash/logo_512.png';
|
||||
import { LoginFormType, loginSchema } from 'screens/auth/login/lib/form';
|
||||
import LanguageSelector from 'screens/auth/select-language/SelectLang';
|
||||
import { useUserStore } from '../lib/userstore';
|
||||
import { Loginstyle } from './styled';
|
||||
|
||||
const Filial = [
|
||||
{ label: 'Toshkent shahar' },
|
||||
{ label: 'Andijon viloyati' },
|
||||
{ label: 'Samarqand viloyati' },
|
||||
{ label: 'Toshkent viloyati' },
|
||||
{ label: 'Xorazm viloyati' },
|
||||
];
|
||||
|
||||
const Login = () => {
|
||||
const { t } = useTranslation();
|
||||
const passportNumberRef = useRef<TextInput>(null);
|
||||
const [filialDropdownVisible, setFilialDropdownVisible] = useState(false);
|
||||
const navigation = useNavigation<NativeStackNavigationProp<any>>();
|
||||
const { setUser, setExpireTime } = useUserStore(state => state);
|
||||
const [error, setError] = useState<string>();
|
||||
const [rawPhone, setRawPhone] = useState('+998');
|
||||
const { data: branchList } = useQuery({
|
||||
queryKey: ['branchList'],
|
||||
queryFn: branchApi.branchList,
|
||||
});
|
||||
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: (payload: loginPayload) => authApi.login(payload),
|
||||
onSuccess: res => {
|
||||
navigation.navigate('Login-Confirm');
|
||||
setExpireTime(res.data.expireTime);
|
||||
},
|
||||
onError: err => {
|
||||
setError('Xatolik yuz berdi');
|
||||
console.dir(err);
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm<LoginFormType>({
|
||||
resolver: zodResolver(loginSchema),
|
||||
defaultValues: {
|
||||
phone: '',
|
||||
passportSeriya: '',
|
||||
passportNumber: '',
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (data: LoginFormType) => {
|
||||
mutate({
|
||||
branchId: data.branchId,
|
||||
phoneNumber: data.phone,
|
||||
passportSerial: `${data.passportSeriya.toUpperCase()}${
|
||||
data.passportNumber
|
||||
}`,
|
||||
});
|
||||
// navigation.navigate('Login-Confirm');
|
||||
setUser({
|
||||
phoneNumber: data.phone,
|
||||
});
|
||||
};
|
||||
|
||||
const handleBackNavigation = useCallback(() => {
|
||||
navigation.navigate('select-auth');
|
||||
}, [navigation]);
|
||||
|
||||
const handlePhoneChange = useCallback((text: string) => {
|
||||
const digits = text.replace(/\D/g, '');
|
||||
const full = digits.startsWith('998') ? digits : `998${digits}`;
|
||||
setRawPhone(`+${full}`);
|
||||
setValue('phone', full, { shouldValidate: true });
|
||||
}, []);
|
||||
|
||||
const keyboardBehavior = useMemo(
|
||||
() => (Platform.OS === 'ios' ? 'padding' : 'height'),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<ImageBackground
|
||||
source={Logo}
|
||||
style={Loginstyle.background}
|
||||
resizeMode="contain"
|
||||
imageStyle={{
|
||||
opacity: 0.3,
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
transform: [{ scale: 1.5 }],
|
||||
}}
|
||||
>
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<View style={Loginstyle.langContainer}>
|
||||
<TouchableOpacity onPress={handleBackNavigation}>
|
||||
<SimpleLineIcons name="arrow-left" color="#000" size={20} />
|
||||
</TouchableOpacity>
|
||||
<LanguageSelector />
|
||||
</View>
|
||||
<KeyboardAvoidingView
|
||||
style={Loginstyle.container}
|
||||
behavior={keyboardBehavior}
|
||||
>
|
||||
<ScrollView style={{ flex: 1 }}>
|
||||
<View style={Loginstyle.scrollContainer}>
|
||||
<View style={Loginstyle.loginContainer}>
|
||||
<Text style={Loginstyle.title}>{t('Tizimga kirish')}</Text>
|
||||
<Controller
|
||||
control={control}
|
||||
name="phone"
|
||||
render={({ field: { onChange } }) => {
|
||||
const formatted = formatPhone(rawPhone);
|
||||
return (
|
||||
<View>
|
||||
<Text style={Loginstyle.label}>
|
||||
{t('Telefon raqami')}
|
||||
</Text>
|
||||
<TextInput
|
||||
keyboardType="numeric"
|
||||
placeholder="+998 90 123-45-67"
|
||||
value={formatted}
|
||||
onChangeText={handlePhoneChange}
|
||||
style={Loginstyle.input}
|
||||
placeholderTextColor="#D8DADC"
|
||||
maxLength={19} // +998 90 123-45-67 bo'lishi uchun
|
||||
/>
|
||||
{errors.phone && (
|
||||
<Text style={Loginstyle.errorText}>
|
||||
{t(errors.phone.message || '')}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<View>
|
||||
<Text style={Loginstyle.label}>
|
||||
{t('Passport seriya raqami')}
|
||||
</Text>
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="passportSeriya"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextInput
|
||||
style={[Loginstyle.input, Loginstyle.seriyaInput]}
|
||||
placeholder="AA"
|
||||
maxLength={2}
|
||||
autoCapitalize="characters"
|
||||
value={value}
|
||||
onChangeText={text => {
|
||||
onChange(text);
|
||||
if (text.length === 2) {
|
||||
passportNumberRef.current?.focus();
|
||||
}
|
||||
}}
|
||||
placeholderTextColor="#D8DADC"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="passportNumber"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextInput
|
||||
ref={passportNumberRef}
|
||||
style={[Loginstyle.input, Loginstyle.raqamInput]}
|
||||
placeholder="1234567"
|
||||
maxLength={7}
|
||||
keyboardType="numeric"
|
||||
value={value}
|
||||
onChangeText={text => {
|
||||
const onlyNumbers = text.replace(/[^0-9]/g, '');
|
||||
onChange(onlyNumbers);
|
||||
}}
|
||||
placeholderTextColor="#D8DADC"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
{(errors.passportSeriya || errors.passportNumber) && (
|
||||
<Text style={Loginstyle.errorText}>
|
||||
{t(errors.passportSeriya?.message || '') ||
|
||||
t(errors.passportNumber?.message || '')}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="branchId"
|
||||
render={({ field: { value } }) => (
|
||||
<View style={{ position: 'relative' }}>
|
||||
<Text style={Loginstyle.label}>{t('Filial')}</Text>
|
||||
<View style={Loginstyle.input}>
|
||||
<TouchableOpacity
|
||||
style={Loginstyle.selector}
|
||||
onPress={() =>
|
||||
setFilialDropdownVisible(prev => !prev)
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
value
|
||||
? { color: '#000' }
|
||||
: Loginstyle.selectedText
|
||||
}
|
||||
>
|
||||
{branchList?.find(e => e.id === value)?.name ||
|
||||
t('Filialni tanlang...')}
|
||||
</Text>
|
||||
<SimpleLineIcons
|
||||
name={
|
||||
filialDropdownVisible ? 'arrow-up' : 'arrow-down'
|
||||
}
|
||||
color="#000"
|
||||
size={14}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
{filialDropdownVisible && (
|
||||
<View style={[Loginstyle.dropdown, { maxHeight: 200 }]}>
|
||||
<ScrollView nestedScrollEnabled>
|
||||
{branchList &&
|
||||
branchList.map((item: Branch) => (
|
||||
<TouchableOpacity
|
||||
key={item.id}
|
||||
style={Loginstyle.dropdownItem}
|
||||
onPress={() => {
|
||||
setValue('branchId', item.id);
|
||||
setFilialDropdownVisible(false);
|
||||
}}
|
||||
>
|
||||
<Text style={Loginstyle.dropdownItemText}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)}
|
||||
{errors.branchId && (
|
||||
<Text style={Loginstyle.errorText}>
|
||||
{t(errors.branchId.message || '')}
|
||||
</Text>
|
||||
)}
|
||||
{error && (
|
||||
<Text style={[Loginstyle.errorText]}>{t(error)}</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={handleSubmit(onSubmit)}
|
||||
style={Loginstyle.button}
|
||||
>
|
||||
{isPending ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<Text style={Loginstyle.btnText}>
|
||||
{t('Tizimga kirish')}
|
||||
</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
<View
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: 14, fontWeight: '500' }}>
|
||||
{t('ID va kabinet yo’qmi?')}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={Loginstyle.dropdownItem}
|
||||
onPress={() => navigation.navigate('Register')}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: '#28A7E8',
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
}}
|
||||
>
|
||||
{t('Ro’yxatdan o’tish')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
</ImageBackground>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
126
src/screens/auth/login/ui/styled.ts
Normal file
126
src/screens/auth/login/ui/styled.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
export const Loginstyle = StyleSheet.create({
|
||||
langContainer: {
|
||||
width: '100%',
|
||||
marginTop: 10,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
background: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
overlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.7)',
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContainer: {
|
||||
flexGrow: 1,
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
},
|
||||
loginContainer: {
|
||||
borderRadius: 20,
|
||||
padding: 30,
|
||||
display: 'flex',
|
||||
gap: 20,
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
},
|
||||
label: {
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
color: '#28A7E8',
|
||||
marginBottom: 5,
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
color: '#28A7E8',
|
||||
marginBottom: 20,
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#D8DADC',
|
||||
borderRadius: 12,
|
||||
padding: 15,
|
||||
fontSize: 16,
|
||||
fontWeight: '400',
|
||||
backgroundColor: '#FFFFFF',
|
||||
color: '#000',
|
||||
},
|
||||
seriyaInput: {
|
||||
width: 60,
|
||||
fontSize: 14,
|
||||
textTransform: 'uppercase',
|
||||
borderTopRightRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
},
|
||||
raqamInput: {
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
},
|
||||
selector: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
},
|
||||
selectedText: {
|
||||
fontSize: 14,
|
||||
color: '#D8DADC',
|
||||
},
|
||||
dropdown: {
|
||||
position: 'absolute',
|
||||
top: 80,
|
||||
left: 0,
|
||||
right: 0,
|
||||
backgroundColor: '#fff',
|
||||
borderWidth: 1,
|
||||
borderColor: '#ccc',
|
||||
borderRadius: 8,
|
||||
zIndex: 10,
|
||||
elevation: 5,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 4,
|
||||
maxHeight: 150,
|
||||
},
|
||||
dropdownItem: {
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 12,
|
||||
},
|
||||
dropdownItemText: {
|
||||
fontSize: 14,
|
||||
color: '#333',
|
||||
},
|
||||
button: {
|
||||
backgroundColor: '#28A7E8',
|
||||
height: 56,
|
||||
borderRadius: 8,
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
btnText: {
|
||||
color: '#fff',
|
||||
fontSize: 20,
|
||||
textAlign: 'center',
|
||||
},
|
||||
errorText: {
|
||||
color: 'red',
|
||||
fontSize: 12,
|
||||
},
|
||||
});
|
||||
20
src/screens/auth/registeration/lib/form.ts
Normal file
20
src/screens/auth/registeration/lib/form.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// form.ts
|
||||
import { z } from 'zod';
|
||||
|
||||
export const FirstStepSchema = z.object({
|
||||
firstName: z.string().min(3, "Eng kamida 3ta belgi bo'lishi kerak"),
|
||||
lastName: z.string().min(3, "Eng kamida 3ta belgi bo'lishi kerak"),
|
||||
phoneNumber: z.string().min(12, 'Xato raqam kiritildi'),
|
||||
branchId: z.number().min(1, 'Filialni tanlang'),
|
||||
recommend: z.string().min(1, 'Majburiy maydon'),
|
||||
});
|
||||
|
||||
export const SecondStepSchema = z.object({
|
||||
passportSeriya: z.string().length(2, '2 ta harf kerak'),
|
||||
birthDate: z.string().min(8, 'Majburiy maydon'),
|
||||
passportNumber: z.string().length(7, '7 ta raqam kerak'),
|
||||
jshshir: z.string().length(14, '14 ta raqam kerak'),
|
||||
});
|
||||
|
||||
export type FirstStepFormType = z.infer<typeof FirstStepSchema>;
|
||||
export type SecondStepFormType = z.infer<typeof SecondStepSchema>;
|
||||
42
src/screens/auth/registeration/lib/modalStore.ts
Normal file
42
src/screens/auth/registeration/lib/modalStore.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface ModalState {
|
||||
isVisible: boolean;
|
||||
title: string;
|
||||
message: string;
|
||||
type: 'success' | 'error' | 'info';
|
||||
onConfirm: () => void;
|
||||
onCancel?: () => void;
|
||||
openModal: (
|
||||
title: string,
|
||||
message: string,
|
||||
type?: 'success' | 'error' | 'info',
|
||||
onConfirm?: () => void,
|
||||
onCancel?: () => void
|
||||
) => void;
|
||||
closeModal: () => void;
|
||||
}
|
||||
|
||||
export const useModalStore = create<ModalState>((set) => ({
|
||||
isVisible: false,
|
||||
title: '',
|
||||
message: '',
|
||||
type: 'info',
|
||||
onConfirm: () => { },
|
||||
onCancel: undefined,
|
||||
|
||||
openModal: (title, message, type = 'info', onConfirm, onCancel) =>
|
||||
set({
|
||||
isVisible: true,
|
||||
title,
|
||||
message,
|
||||
type,
|
||||
onConfirm: () => {
|
||||
if (onConfirm) onConfirm();
|
||||
set({ isVisible: false });
|
||||
},
|
||||
onCancel: onCancel || undefined,
|
||||
}),
|
||||
|
||||
closeModal: () => set({ isVisible: false }),
|
||||
}));
|
||||
28
src/screens/auth/registeration/lib/userstore.ts
Normal file
28
src/screens/auth/registeration/lib/userstore.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface UserState {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
phoneNumber: string;
|
||||
setUser: (user: Partial<UserState>) => void;
|
||||
clearUser: () => void;
|
||||
}
|
||||
|
||||
export const useUserStore = create<UserState>(set => ({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
phoneNumber: '',
|
||||
|
||||
setUser: user =>
|
||||
set(state => ({
|
||||
...state,
|
||||
...user,
|
||||
})),
|
||||
|
||||
clearUser: () =>
|
||||
set({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
phoneNumber: '',
|
||||
}),
|
||||
}));
|
||||
320
src/screens/auth/registeration/ui/Confirm.tsx
Normal file
320
src/screens/auth/registeration/ui/Confirm.tsx
Normal file
@@ -0,0 +1,320 @@
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { authApi } from 'api/auth';
|
||||
import { otpPayload, resendPayload } from 'api/auth/type';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
ImageBackground,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import SimpleLineIcons from 'react-native-vector-icons/SimpleLineIcons';
|
||||
import Logo from 'screens/../../assets/bootsplash/logo_512.png';
|
||||
import { useModalStore } from 'screens/auth/registeration/lib/modalStore';
|
||||
import LanguageSelector from 'screens/auth/select-language/SelectLang';
|
||||
import { RootStackParamList } from 'types/types';
|
||||
import { useUserStore } from '../lib/userstore';
|
||||
import { RegisterStyle } from './styled';
|
||||
|
||||
type VerificationCodeScreenNavigationProp = NativeStackNavigationProp<
|
||||
RootStackParamList,
|
||||
'Confirm'
|
||||
>;
|
||||
|
||||
const OTP_LENGTH = 4;
|
||||
|
||||
const Confirm = ({
|
||||
setStep,
|
||||
}: {
|
||||
setStep: React.Dispatch<React.SetStateAction<number>>;
|
||||
}) => {
|
||||
const navigation = useNavigation<VerificationCodeScreenNavigationProp>();
|
||||
const { t } = useTranslation();
|
||||
const [code, setCode] = useState<string[]>(new Array(OTP_LENGTH).fill(''));
|
||||
const [timer, setTimer] = useState(60);
|
||||
const [canResend, setCanResend] = useState(false);
|
||||
const [errorConfirm, setErrorConfirm] = useState<string | null>(null);
|
||||
const inputRefs = useRef<Array<TextInput | null>>([]);
|
||||
const { phoneNumber } = useUserStore(state => state);
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: (payload: otpPayload) => authApi.verifyOtp(payload),
|
||||
onSuccess: async res => {
|
||||
await AsyncStorage.setItem('token', res.data.accessToken);
|
||||
navigation.navigate('Confirm');
|
||||
setErrorConfirm(null);
|
||||
},
|
||||
onError: (err: any) => {
|
||||
setErrorConfirm(err?.response.data.message);
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: resendMutate } = useMutation({
|
||||
mutationFn: (payload: resendPayload) => authApi.resendOtp(payload),
|
||||
onSuccess: async res => {
|
||||
setTimer(60);
|
||||
setCanResend(false);
|
||||
setCode(new Array(OTP_LENGTH).fill(''));
|
||||
inputRefs.current[0]?.focus();
|
||||
setErrorConfirm(null);
|
||||
},
|
||||
onError: (err: any) => {
|
||||
setErrorConfirm(err?.response.data.message);
|
||||
},
|
||||
});
|
||||
|
||||
const openModal = useModalStore(state => state.openModal);
|
||||
useEffect(() => {
|
||||
let interval: NodeJS.Timeout | null = null;
|
||||
if (timer > 0) {
|
||||
interval = setInterval(() => {
|
||||
setTimer(prevTimer => prevTimer - 1);
|
||||
}, 1000);
|
||||
} else {
|
||||
setCanResend(true);
|
||||
if (interval) clearInterval(interval);
|
||||
}
|
||||
return () => {
|
||||
if (interval) clearInterval(interval);
|
||||
};
|
||||
}, [timer]);
|
||||
|
||||
const handleCodeChange = (text: string, index: number) => {
|
||||
const newCode = [...code];
|
||||
newCode[index] = text;
|
||||
setCode(newCode);
|
||||
|
||||
if (text.length > 0 && index < OTP_LENGTH - 1) {
|
||||
inputRefs.current[index + 1]?.focus();
|
||||
}
|
||||
if (text.length === 0 && index > 0) {
|
||||
inputRefs.current[index - 1]?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyPress = ({ nativeEvent }: any, index: number) => {
|
||||
if (nativeEvent.key === 'Backspace' && code[index] === '' && index > 0) {
|
||||
inputRefs.current[index - 1]?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const handleResendCode = () => {
|
||||
resendMutate({
|
||||
phoneNumber: phoneNumber,
|
||||
otpType: 'REGISTRATION',
|
||||
});
|
||||
};
|
||||
|
||||
const handleVerifyCode = () => {
|
||||
const enteredCode = code.join('');
|
||||
mutate({
|
||||
phoneNumber: phoneNumber,
|
||||
otp: String(enteredCode),
|
||||
otpType: 'REGISTRATION',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ImageBackground
|
||||
source={Logo}
|
||||
style={RegisterStyle.background}
|
||||
resizeMode="contain"
|
||||
imageStyle={{
|
||||
opacity: 0.3,
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
transform: [{ scale: 1.5 }],
|
||||
}}
|
||||
>
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<View style={styles.langContainer}>
|
||||
<TouchableOpacity onPress={() => setStep(1)}>
|
||||
<SimpleLineIcons name="arrow-left" color="#000" size={20} />
|
||||
</TouchableOpacity>
|
||||
<LanguageSelector />
|
||||
</View>
|
||||
<KeyboardAvoidingView
|
||||
style={styles.container}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
>
|
||||
<View style={styles.content}>
|
||||
<Text style={styles.title}>{t('Tasdiqlash kodini kiriting')}</Text>
|
||||
<Text style={styles.message}>
|
||||
{phoneNumber} {t('raqamiga yuborilgan')} {OTP_LENGTH}{' '}
|
||||
{t('xonali kodni kiriting.')}
|
||||
</Text>
|
||||
<View style={styles.otpContainer}>
|
||||
{code.map((digit, index) => (
|
||||
<TextInput
|
||||
key={index}
|
||||
ref={ref => {
|
||||
inputRefs.current[index] = ref;
|
||||
}}
|
||||
style={styles.otpInput}
|
||||
keyboardType="number-pad"
|
||||
maxLength={1}
|
||||
onChangeText={text => handleCodeChange(text, index)}
|
||||
onKeyPress={e => handleKeyPress(e, index)}
|
||||
value={digit}
|
||||
autoFocus={index === 0}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
{errorConfirm !== null && (
|
||||
<Text style={styles.errorText}>{errorConfirm}</Text>
|
||||
)}
|
||||
<View style={styles.resendContainer}>
|
||||
{canResend ? (
|
||||
<TouchableOpacity
|
||||
onPress={handleResendCode}
|
||||
style={styles.resendButton}
|
||||
>
|
||||
<Text style={styles.resendButtonText}>
|
||||
{t('Kodni qayta yuborish')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<Text style={styles.timerText}>
|
||||
{t('Kodni qayta yuborish vaqti')} ({timer}s)
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={styles.verifyButton}
|
||||
onPress={handleVerifyCode}
|
||||
>
|
||||
{isPending ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<Text
|
||||
style={[
|
||||
RegisterStyle.btnText,
|
||||
isPending && RegisterStyle.buttonTextDisabled,
|
||||
]}
|
||||
>
|
||||
{t('Tasdiqlash')}
|
||||
</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
</ImageBackground>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safeArea: {
|
||||
flex: 1,
|
||||
backgroundColor: '#f8f9fa',
|
||||
},
|
||||
errorText: {
|
||||
color: 'red',
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
marginTop: 10,
|
||||
textAlign: 'center',
|
||||
},
|
||||
langContainer: {
|
||||
width: '100%',
|
||||
marginTop: 10,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
color: '#333',
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 20,
|
||||
},
|
||||
content: {
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
color: '#333',
|
||||
marginBottom: 10,
|
||||
textAlign: 'center',
|
||||
},
|
||||
message: {
|
||||
fontSize: 15,
|
||||
color: '#666',
|
||||
textAlign: 'center',
|
||||
marginBottom: 30,
|
||||
lineHeight: 22,
|
||||
},
|
||||
otpContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
},
|
||||
otpInput: {
|
||||
width: 45,
|
||||
height: 55,
|
||||
borderWidth: 1,
|
||||
borderColor: '#ccc',
|
||||
borderRadius: 8,
|
||||
textAlign: 'center',
|
||||
fontSize: 22,
|
||||
fontWeight: 'bold',
|
||||
color: '#333',
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
resendContainer: {
|
||||
marginBottom: 30,
|
||||
},
|
||||
timerText: {
|
||||
fontSize: 15,
|
||||
color: '#999',
|
||||
},
|
||||
resendButton: {
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 20,
|
||||
borderRadius: 8,
|
||||
},
|
||||
resendButtonText: {
|
||||
fontSize: 15,
|
||||
color: '#007bff',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
verifyButton: {
|
||||
backgroundColor: '#007bff',
|
||||
paddingVertical: 15,
|
||||
paddingHorizontal: 40,
|
||||
borderRadius: 8,
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
elevation: 3,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 4,
|
||||
},
|
||||
verifyButtonText: {
|
||||
color: '#fff',
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
||||
export default Confirm;
|
||||
468
src/screens/auth/registeration/ui/FirstStep.tsx
Normal file
468
src/screens/auth/registeration/ui/FirstStep.tsx
Normal file
@@ -0,0 +1,468 @@
|
||||
'use client';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import {
|
||||
type RouteProp,
|
||||
useNavigation,
|
||||
useRoute,
|
||||
} from '@react-navigation/native';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { authApi } from 'api/auth';
|
||||
import { registerPayload } from 'api/auth/type';
|
||||
import { Branch, branchApi } from 'api/branch';
|
||||
import formatPhone from 'helpers/formatPhone';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Animated,
|
||||
ImageBackground,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
ScrollView,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import AntDesign from 'react-native-vector-icons/AntDesign';
|
||||
import SimpleLineIcons from 'react-native-vector-icons/SimpleLineIcons';
|
||||
import Logo from 'screens/../../assets/bootsplash/logo_512.png';
|
||||
import {
|
||||
FirstStepFormType,
|
||||
FirstStepSchema,
|
||||
} from 'screens/auth/registeration/lib/form';
|
||||
import LanguageSelector from 'screens/auth/select-language/SelectLang';
|
||||
import { RootStackParamList } from 'types/types';
|
||||
import { useUserStore } from '../lib/userstore';
|
||||
import { RegisterStyle } from './styled';
|
||||
|
||||
type LoginScreenNavigationProp = NativeStackNavigationProp<
|
||||
RootStackParamList,
|
||||
'Login'
|
||||
>;
|
||||
|
||||
const recommended = [
|
||||
{ label: 'Tanishim orqali', value: 'FRIEND' },
|
||||
{ label: 'Telegram orqali', value: 'TELEGRAM' },
|
||||
{ label: 'Instagram orqali', value: 'INSTAGRAM' },
|
||||
{ label: 'Facebook orqali', value: 'FACEBOOK' },
|
||||
];
|
||||
|
||||
const FirstStep = ({ onNext }: { onNext: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const [filialDropdownVisible, setFilialDropdownVisible] = useState(false);
|
||||
const [error, setError] = useState<string>();
|
||||
const { setUser } = useUserStore(state => state);
|
||||
const { data: branchList } = useQuery({
|
||||
queryKey: ['branchList'],
|
||||
queryFn: branchApi.branchList,
|
||||
});
|
||||
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: (payload: registerPayload) => authApi.register(payload),
|
||||
onSuccess: res => {
|
||||
onNext();
|
||||
},
|
||||
onError: err => {
|
||||
console.dir(err);
|
||||
|
||||
setError('Xatolik yuz berdi');
|
||||
},
|
||||
});
|
||||
|
||||
const [recommendedDropdownVisible, setRecommendedDropdownVisible] =
|
||||
useState(false);
|
||||
const [termsAccepted, setTermsAccepted] = useState(false);
|
||||
const [checkboxAnimation] = useState(new Animated.Value(0));
|
||||
const navigation = useNavigation<LoginScreenNavigationProp>();
|
||||
const [rawPhone, setRawPhone] = useState('+998');
|
||||
const route = useRoute<RouteProp<RootStackParamList, 'Register'>>();
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
getValues,
|
||||
} = useForm<FirstStepFormType>({
|
||||
resolver: zodResolver(FirstStepSchema),
|
||||
defaultValues: {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
recommend: '',
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (data: FirstStepFormType) => {
|
||||
setUser({
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
phoneNumber: data.phoneNumber,
|
||||
});
|
||||
mutate(data);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (route.params?.termsAccepted) {
|
||||
setTermsAccepted(true);
|
||||
Animated.spring(checkboxAnimation, {
|
||||
toValue: 1,
|
||||
useNativeDriver: false,
|
||||
tension: 100,
|
||||
friction: 8,
|
||||
}).start();
|
||||
}
|
||||
}, [route.params]);
|
||||
|
||||
const navigateToTerms = () => {
|
||||
navigation.navigate('TermsAndConditions');
|
||||
setTermsAccepted(true);
|
||||
Animated.spring(checkboxAnimation, {
|
||||
toValue: 1,
|
||||
useNativeDriver: false,
|
||||
tension: 100,
|
||||
friction: 8,
|
||||
}).start();
|
||||
};
|
||||
|
||||
const toggleCheckbox = () => {
|
||||
if (!termsAccepted) {
|
||||
navigateToTerms();
|
||||
} else {
|
||||
setTermsAccepted(false);
|
||||
Animated.spring(checkboxAnimation, {
|
||||
toValue: 0,
|
||||
useNativeDriver: false,
|
||||
tension: 100,
|
||||
friction: 8,
|
||||
}).start();
|
||||
}
|
||||
};
|
||||
return (
|
||||
<ImageBackground
|
||||
source={Logo}
|
||||
style={RegisterStyle.background}
|
||||
resizeMode="contain"
|
||||
imageStyle={{
|
||||
opacity: 0.3,
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
transform: [{ scale: 1.5 }],
|
||||
}}
|
||||
>
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<View style={RegisterStyle.langContainer}>
|
||||
<TouchableOpacity onPress={() => navigation.navigate('select-auth')}>
|
||||
<SimpleLineIcons name="arrow-left" color="#000" size={20} />
|
||||
</TouchableOpacity>
|
||||
<LanguageSelector />
|
||||
</View>
|
||||
<KeyboardAvoidingView
|
||||
style={RegisterStyle.container}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
>
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
style={RegisterStyle.content}
|
||||
>
|
||||
<View style={RegisterStyle.scrollContainer}>
|
||||
<View style={RegisterStyle.loginContainer}>
|
||||
<Text style={RegisterStyle.title}>
|
||||
{t("Ro'yxatdan o'tish")}
|
||||
</Text>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="firstName"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<View>
|
||||
<Text style={RegisterStyle.label}>{t('Ism')}</Text>
|
||||
<TextInput
|
||||
style={RegisterStyle.input}
|
||||
placeholder={t('Ismingiz')}
|
||||
onChangeText={onChange}
|
||||
value={value}
|
||||
placeholderTextColor={'#D8DADC'}
|
||||
/>
|
||||
{errors.firstName && (
|
||||
<Text style={RegisterStyle.errorText}>
|
||||
{t(errors.firstName.message || '')}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="lastName"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<View>
|
||||
<Text style={RegisterStyle.label}>{t('Familiya')}</Text>
|
||||
<TextInput
|
||||
style={RegisterStyle.input}
|
||||
placeholder={t('Familiyangiz')}
|
||||
placeholderTextColor={'#D8DADC'}
|
||||
onChangeText={onChange}
|
||||
value={value}
|
||||
/>
|
||||
{errors.lastName && (
|
||||
<Text style={RegisterStyle.errorText}>
|
||||
{t(errors.lastName.message || '')}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="phoneNumber"
|
||||
render={({ field: { onChange } }) => {
|
||||
const formatted = formatPhone(rawPhone);
|
||||
return (
|
||||
<View>
|
||||
<Text style={RegisterStyle.label}>
|
||||
{t('Telefon raqami')}
|
||||
</Text>
|
||||
<TextInput
|
||||
keyboardType="numeric"
|
||||
placeholder="+998 __ ___-__-__"
|
||||
value={formatted}
|
||||
onChangeText={text => {
|
||||
const digits = text.replace(/\D/g, '').slice(0, 12);
|
||||
const full = digits.startsWith('998')
|
||||
? digits
|
||||
: `998${digits}`;
|
||||
setRawPhone(full);
|
||||
onChange(full);
|
||||
}}
|
||||
style={RegisterStyle.input}
|
||||
placeholderTextColor="#D8DADC"
|
||||
maxLength={17}
|
||||
/>
|
||||
{errors.phoneNumber && (
|
||||
<Text style={RegisterStyle.errorText}>
|
||||
{t(errors.phoneNumber.message || '')}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="branchId"
|
||||
render={({ field: { value } }) => (
|
||||
<View style={{ position: 'relative' }}>
|
||||
<Text style={RegisterStyle.label}>{t('Filial')}</Text>
|
||||
<View style={RegisterStyle.input}>
|
||||
<TouchableOpacity
|
||||
style={RegisterStyle.selector}
|
||||
onPress={() =>
|
||||
setFilialDropdownVisible(prev => !prev)
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
value
|
||||
? { color: '#000' }
|
||||
: RegisterStyle.selectedText
|
||||
}
|
||||
>
|
||||
{branchList?.find(e => e.id === value)?.name ||
|
||||
t('Filialni tanlang...')}
|
||||
</Text>
|
||||
<SimpleLineIcons
|
||||
name={
|
||||
filialDropdownVisible ? 'arrow-up' : 'arrow-down'
|
||||
}
|
||||
color="#000"
|
||||
size={14}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
{filialDropdownVisible && (
|
||||
<View
|
||||
style={[RegisterStyle.dropdown, { maxHeight: 200 }]}
|
||||
>
|
||||
<ScrollView nestedScrollEnabled>
|
||||
{branchList &&
|
||||
branchList.map((item: Branch) => (
|
||||
<TouchableOpacity
|
||||
key={item.id}
|
||||
style={RegisterStyle.dropdownItem}
|
||||
onPress={() => {
|
||||
setValue('branchId', item.id);
|
||||
setFilialDropdownVisible(false);
|
||||
}}
|
||||
>
|
||||
<Text style={RegisterStyle.dropdownItemText}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)}
|
||||
{errors.branchId && (
|
||||
<Text style={RegisterStyle.errorText}>
|
||||
{t(errors.branchId.message || '')}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="recommend"
|
||||
render={({ field: { value } }) => (
|
||||
<View style={{ position: 'relative' }}>
|
||||
<Text style={RegisterStyle.label}>
|
||||
{t('Bizni qaerdan topdingiz?')}
|
||||
</Text>
|
||||
<View style={RegisterStyle.input}>
|
||||
<TouchableOpacity
|
||||
style={RegisterStyle.selector}
|
||||
onPress={() =>
|
||||
setRecommendedDropdownVisible(prev => !prev)
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
value
|
||||
? { color: '#000' }
|
||||
: RegisterStyle.selectedText
|
||||
}
|
||||
>
|
||||
{t(
|
||||
recommended.find(e => e.value === value)?.label ||
|
||||
'Bizni kim tavsiya qildi...',
|
||||
)}
|
||||
</Text>
|
||||
<SimpleLineIcons
|
||||
name={
|
||||
recommendedDropdownVisible
|
||||
? 'arrow-up'
|
||||
: 'arrow-down'
|
||||
}
|
||||
color="#000"
|
||||
size={14}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
{recommendedDropdownVisible && (
|
||||
<View
|
||||
style={[RegisterStyle.dropdown, { maxHeight: 200 }]}
|
||||
>
|
||||
<ScrollView nestedScrollEnabled>
|
||||
{recommended.map((item, index) => (
|
||||
<TouchableOpacity
|
||||
key={index}
|
||||
style={RegisterStyle.dropdownItem}
|
||||
onPress={() => {
|
||||
setValue('recommend', item.value);
|
||||
setRecommendedDropdownVisible(false);
|
||||
}}
|
||||
>
|
||||
<Text style={RegisterStyle.dropdownItemText}>
|
||||
{t(item.label)}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)}
|
||||
{errors.recommend && (
|
||||
<Text style={RegisterStyle.errorText}>
|
||||
{t(errors.recommend.message || '')}
|
||||
</Text>
|
||||
)}
|
||||
{error && (
|
||||
<Text style={[RegisterStyle.errorText]}>
|
||||
{t(error)}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
<View style={RegisterStyle.termsContainer}>
|
||||
<TouchableOpacity
|
||||
style={RegisterStyle.checkboxContainer}
|
||||
onPress={toggleCheckbox}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Animated.View
|
||||
style={[
|
||||
RegisterStyle.checkbox,
|
||||
termsAccepted && RegisterStyle.checkboxChecked,
|
||||
{
|
||||
transform: [
|
||||
{
|
||||
scale: checkboxAnimation.interpolate({
|
||||
inputRange: [0, 0.5, 1],
|
||||
outputRange: [1, 1.1, 1],
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
>
|
||||
{termsAccepted && (
|
||||
<Animated.View
|
||||
style={{
|
||||
opacity: checkboxAnimation,
|
||||
transform: [
|
||||
{
|
||||
scale: checkboxAnimation,
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
<AntDesign name="check" color="#fff" size={20} />
|
||||
</Animated.View>
|
||||
)}
|
||||
</Animated.View>
|
||||
<View style={RegisterStyle.termsTextContainer}>
|
||||
<Text style={RegisterStyle.termsText}>
|
||||
<Text>{t('Foydalanish shartlari')}</Text>
|
||||
<Text> {t('bilan tanishib chiqdim!')}</Text>
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={handleSubmit(onSubmit)}
|
||||
style={[
|
||||
RegisterStyle.button,
|
||||
(!termsAccepted || isPending) &&
|
||||
RegisterStyle.buttonDisabled,
|
||||
]}
|
||||
disabled={!termsAccepted || isPending}
|
||||
>
|
||||
{isPending ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<Text
|
||||
style={[
|
||||
RegisterStyle.btnText,
|
||||
(!termsAccepted || isPending) &&
|
||||
RegisterStyle.buttonTextDisabled,
|
||||
]}
|
||||
>
|
||||
{t('Davom etish')}
|
||||
</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
</ImageBackground>
|
||||
);
|
||||
};
|
||||
|
||||
export default FirstStep;
|
||||
427
src/screens/auth/registeration/ui/SecondStep.tsx
Normal file
427
src/screens/auth/registeration/ui/SecondStep.tsx
Normal file
@@ -0,0 +1,427 @@
|
||||
'use client';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import {
|
||||
useNavigation,
|
||||
useRoute,
|
||||
type RouteProp,
|
||||
} from '@react-navigation/native';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import passportApi, { sendPassportPayload } from 'api/passport';
|
||||
import DatePickerInput from 'components/DatePicker';
|
||||
import SingleFileDrop from 'components/FileDrop';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Animated,
|
||||
Dimensions,
|
||||
Image,
|
||||
ImageBackground,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
ScrollView,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import AntDesign from 'react-native-vector-icons/AntDesign';
|
||||
import SimpleLineIcons from 'react-native-vector-icons/SimpleLineIcons';
|
||||
import Logo from 'screens/../../assets/bootsplash/logo_512.png';
|
||||
import LanguageSelector from 'screens/auth/select-language/SelectLang';
|
||||
import { RootStackParamList } from 'types/types';
|
||||
import { SecondStepFormType, SecondStepSchema } from '../lib/form';
|
||||
import { RegisterStyle } from './styled';
|
||||
|
||||
interface FileData {
|
||||
uri: string;
|
||||
name: string;
|
||||
type: string;
|
||||
base64: string;
|
||||
}
|
||||
|
||||
const SecondStep = () => {
|
||||
const { t } = useTranslation();
|
||||
const windowWidth = Dimensions.get('window').width;
|
||||
const [frontImage, setFrontImage] = useState<FileData | null>(null);
|
||||
const [backImage, setBackImage] = useState<FileData | null>(null);
|
||||
const isSmallScreen = windowWidth < 200;
|
||||
const [termsAccepted, setTermsAccepted] = useState(true);
|
||||
const passportNumberRef = useRef<TextInput>(null);
|
||||
const [checkboxAnimation] = useState(new Animated.Value(1));
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<RootStackParamList, 'Login'>>();
|
||||
const route = useRoute<RouteProp<RootStackParamList, 'Register'>>();
|
||||
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
|
||||
const [isDatePickerVisible, setDatePickerVisibility] = useState(false);
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const year = date.getFullYear();
|
||||
return `${day}/${month}/${year}`;
|
||||
};
|
||||
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: (payload: sendPassportPayload) =>
|
||||
passportApi.sendPassport(payload),
|
||||
onSuccess: res => {
|
||||
navigation.navigate('Home');
|
||||
},
|
||||
onError: err => {
|
||||
console.dir(err);
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (route.params?.termsAccepted) {
|
||||
setTermsAccepted(true);
|
||||
Animated.spring(checkboxAnimation, {
|
||||
toValue: 1,
|
||||
useNativeDriver: false,
|
||||
tension: 100,
|
||||
friction: 8,
|
||||
}).start();
|
||||
}
|
||||
}, [route.params]);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm<SecondStepFormType>({
|
||||
resolver: zodResolver(SecondStepSchema),
|
||||
defaultValues: {
|
||||
passportNumber: '',
|
||||
passportSeriya: '',
|
||||
birthDate: '',
|
||||
jshshir: '',
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (data: SecondStepFormType) => {
|
||||
const [d, m, y] = data.birthDate.split('/');
|
||||
const isoBirthDate = `${y}-${m.padStart(2, '0')}-${d.padStart(2, '0')}`;
|
||||
|
||||
mutate({
|
||||
fullName: data.passportSeriya.toUpperCase(),
|
||||
birthDate: isoBirthDate,
|
||||
passportSerial: `${data.passportSeriya.toUpperCase()}${
|
||||
data.passportNumber
|
||||
}`,
|
||||
passportPin: data.jshshir,
|
||||
passportFrontImage: frontImage ? `${frontImage.base64}` : '',
|
||||
passportBackImage: backImage ? `${backImage.base64}` : '',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ImageBackground
|
||||
source={Logo}
|
||||
style={RegisterStyle.background}
|
||||
resizeMode="contain"
|
||||
imageStyle={{
|
||||
opacity: 0.3,
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
transform: [{ scale: 1.5 }],
|
||||
}}
|
||||
>
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<View style={RegisterStyle.langContainer}>
|
||||
<TouchableOpacity onPress={() => navigation.goBack()}>
|
||||
<SimpleLineIcons name="arrow-left" color="#000" size={20} />
|
||||
</TouchableOpacity>
|
||||
<LanguageSelector />
|
||||
</View>
|
||||
|
||||
<KeyboardAvoidingView
|
||||
style={RegisterStyle.container}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
>
|
||||
<ScrollView style={RegisterStyle.content}>
|
||||
<View style={RegisterStyle.scrollContainer}>
|
||||
<View style={RegisterStyle.loginContainer}>
|
||||
<Text style={RegisterStyle.title}>
|
||||
{t('Shaxsiy maʼlumotlar')}
|
||||
</Text>
|
||||
<Image
|
||||
source={require('screens/../../assets/bootsplash/passport-sample.jpg')}
|
||||
style={{ width: '100%', height: 300, borderRadius: 8 }}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
<View>
|
||||
{/* PASSPORT */}
|
||||
<Text style={RegisterStyle.label}>
|
||||
{t('Passport seriya raqami')}
|
||||
</Text>
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="passportSeriya"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextInput
|
||||
style={[
|
||||
RegisterStyle.input,
|
||||
RegisterStyle.seriyaInput,
|
||||
]}
|
||||
placeholder="AA"
|
||||
maxLength={2}
|
||||
autoCapitalize="characters"
|
||||
value={value}
|
||||
onChangeText={text => {
|
||||
onChange(text);
|
||||
if (text.length === 2)
|
||||
passportNumberRef.current?.focus();
|
||||
}}
|
||||
placeholderTextColor="#D8DADC"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="passportNumber"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextInput
|
||||
ref={passportNumberRef}
|
||||
style={[
|
||||
RegisterStyle.input,
|
||||
RegisterStyle.raqamInput,
|
||||
]}
|
||||
placeholder="1234567"
|
||||
maxLength={7}
|
||||
keyboardType="numeric"
|
||||
value={value}
|
||||
onChangeText={text => {
|
||||
const onlyNumbers = text.replace(/[^0-9]/g, '');
|
||||
onChange(onlyNumbers);
|
||||
}}
|
||||
placeholderTextColor="#D8DADC"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
{(errors.passportSeriya || errors.passportNumber) && (
|
||||
<Text style={RegisterStyle.errorText}>
|
||||
{t(errors.passportSeriya?.message || '') ||
|
||||
t(errors.passportNumber?.message || '')}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
{/* JSHSHIR */}
|
||||
<Controller
|
||||
control={control}
|
||||
name="jshshir"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<View>
|
||||
<Text style={RegisterStyle.label}>{t('JSHSHIR')}</Text>
|
||||
<TextInput
|
||||
style={RegisterStyle.input}
|
||||
placeholder="12345678901234"
|
||||
placeholderTextColor="#D8DADC"
|
||||
keyboardType="numeric"
|
||||
maxLength={14}
|
||||
value={value}
|
||||
onChangeText={text =>
|
||||
onChange(text.replace(/[^0-9]/g, ''))
|
||||
}
|
||||
/>
|
||||
{errors.jshshir && (
|
||||
<Text style={RegisterStyle.errorText}>
|
||||
{t(errors.jshshir.message || '')}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* BIRTH DATE */}
|
||||
<Controller
|
||||
control={control}
|
||||
name="birthDate"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<View style={{ marginBottom: 10 }}>
|
||||
<Text style={RegisterStyle.label}>
|
||||
{t("Tug'ilgan sana")}
|
||||
</Text>
|
||||
|
||||
<View
|
||||
style={[
|
||||
RegisterStyle.inputContainer,
|
||||
{ paddingHorizontal: 0 },
|
||||
]}
|
||||
>
|
||||
<TextInput
|
||||
style={[
|
||||
RegisterStyle.input,
|
||||
{ flex: 1, borderWidth: 0 },
|
||||
]}
|
||||
placeholder="dd/mm/yyyy"
|
||||
placeholderTextColor="#D8DADC"
|
||||
keyboardType="numeric"
|
||||
value={value}
|
||||
onChangeText={text => {
|
||||
let cleaned = text
|
||||
.replace(/[^\d]/g, '')
|
||||
.slice(0, 8);
|
||||
let formatted = '';
|
||||
|
||||
// Early validation for day and month before formatting
|
||||
if (cleaned.length >= 1) {
|
||||
const firstDigit = cleaned[0];
|
||||
if (firstDigit > '3') return; // Day can't start with 4-9
|
||||
}
|
||||
|
||||
if (cleaned.length >= 2) {
|
||||
const day = parseInt(cleaned.slice(0, 2), 10);
|
||||
if (day > 31 || day === 0) return;
|
||||
}
|
||||
|
||||
if (cleaned.length >= 3) {
|
||||
const monthFirstDigit = cleaned[2];
|
||||
if (monthFirstDigit > '1') return; // Month can't start with 2-9
|
||||
}
|
||||
|
||||
if (cleaned.length >= 4) {
|
||||
const month = parseInt(cleaned.slice(2, 4), 10);
|
||||
if (month > 12 || month === 0) return;
|
||||
}
|
||||
|
||||
// Now format it after initial checks
|
||||
if (cleaned.length <= 2) {
|
||||
formatted = cleaned;
|
||||
} else if (cleaned.length <= 4) {
|
||||
formatted = `${cleaned.slice(
|
||||
0,
|
||||
2,
|
||||
)}/${cleaned.slice(2)}`;
|
||||
} else {
|
||||
formatted = `${cleaned.slice(
|
||||
0,
|
||||
2,
|
||||
)}/${cleaned.slice(2, 4)}/${cleaned.slice(4)}`;
|
||||
}
|
||||
|
||||
// Validate full date not in future
|
||||
if (formatted.length === 10) {
|
||||
const [d, m, y] = formatted.split('/');
|
||||
const inputDate = new Date(+y, +m - 1, +d);
|
||||
const today = new Date();
|
||||
inputDate.setHours(0, 0, 0, 0);
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
if (inputDate > today) return;
|
||||
}
|
||||
|
||||
setValue('birthDate', formatted);
|
||||
}}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
onPress={() => setDatePickerVisibility(true)}
|
||||
style={{ right: 15 }}
|
||||
>
|
||||
<AntDesign
|
||||
name="calendar"
|
||||
color="#D8DADC"
|
||||
size={25}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{errors.birthDate && (
|
||||
<Text style={RegisterStyle.errorText}>
|
||||
{t(errors.birthDate?.message || '')}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
|
||||
<DatePickerInput
|
||||
showPicker={isDatePickerVisible}
|
||||
setShowPicker={setDatePickerVisibility}
|
||||
value={selectedDate || new Date()}
|
||||
onChange={date => {
|
||||
if (date) {
|
||||
const formattedDate = formatDate(date);
|
||||
setSelectedDate(date);
|
||||
setInputValue(formattedDate);
|
||||
setValue('birthDate', formattedDate);
|
||||
}
|
||||
}}
|
||||
maximumDate={new Date()}
|
||||
/>
|
||||
|
||||
{/* FILE UPLOAD */}
|
||||
<Text style={RegisterStyle.mainTitle}>
|
||||
{t('Passport/ID karta rasmi yoki faylni yuklang')}
|
||||
</Text>
|
||||
<View
|
||||
style={[
|
||||
RegisterStyle.sectionsContainer,
|
||||
{ flexDirection: isSmallScreen ? 'column' : 'row' },
|
||||
]}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: isSmallScreen ? '100%' : '48%',
|
||||
marginBottom: 10,
|
||||
}}
|
||||
>
|
||||
<SingleFileDrop
|
||||
title={t('Old tomon')}
|
||||
onFileSelected={setFrontImage}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
width: isSmallScreen ? '100%' : '48%',
|
||||
marginBottom: 10,
|
||||
}}
|
||||
>
|
||||
<SingleFileDrop
|
||||
title={t('Orqa tomon')}
|
||||
onFileSelected={setBackImage}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* BUTTON */}
|
||||
<TouchableOpacity
|
||||
onPress={handleSubmit(onSubmit)}
|
||||
style={[
|
||||
RegisterStyle.button,
|
||||
!termsAccepted && RegisterStyle.buttonDisabled,
|
||||
]}
|
||||
disabled={!termsAccepted}
|
||||
>
|
||||
{isPending ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<Text
|
||||
style={[
|
||||
RegisterStyle.btnText,
|
||||
!termsAccepted && RegisterStyle.buttonTextDisabled,
|
||||
]}
|
||||
>
|
||||
{t('Tasdiqlash')}
|
||||
</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
</ImageBackground>
|
||||
);
|
||||
};
|
||||
|
||||
export default SecondStep;
|
||||
155
src/screens/auth/registeration/ui/TermsAndConditions.tsx
Normal file
155
src/screens/auth/registeration/ui/TermsAndConditions.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import SimpleLineIcons from 'react-native-vector-icons/SimpleLineIcons';
|
||||
import { RootStackParamList } from 'types/types';
|
||||
|
||||
type TermsScreenNavigationProp = NativeStackNavigationProp<
|
||||
RootStackParamList,
|
||||
'TermsAndConditions'
|
||||
>;
|
||||
|
||||
const TermsAndConditions = () => {
|
||||
const navigation = useNavigation<TermsScreenNavigationProp>();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleAgree = () => {
|
||||
navigation.goBack();
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity onPress={() => navigation.goBack()}>
|
||||
<SimpleLineIcons name="arrow-left" color="#000" size={20} />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.headerTitle}>{t('Foydalanish shartlari')}</Text>
|
||||
<View style={{ width: 20 }} />
|
||||
</View>
|
||||
|
||||
<ScrollView style={styles.content} showsVerticalScrollIndicator={false}>
|
||||
<Text style={styles.title}>
|
||||
{t('foydalanish_shartlari_va_qoidalari')}
|
||||
</Text>
|
||||
|
||||
<Text style={styles.sectionTitle}>{t('umumiy_qoidalar')}</Text>
|
||||
<Text style={styles.text}>{t('umumiy_qoidalar_text')}</Text>
|
||||
|
||||
<Text style={styles.sectionTitle}>
|
||||
{t('foydalanuvchi_majburiyatlari')}
|
||||
</Text>
|
||||
<Text style={styles.text}>
|
||||
{t('foydalanuvchi_majburiyatlari_text')}
|
||||
</Text>
|
||||
|
||||
<Text style={styles.sectionTitle}>{t('maxfiylik_siyosati')}</Text>
|
||||
<Text style={styles.text}>{t('maxfiylik_siyosati_text')}</Text>
|
||||
|
||||
<Text style={styles.sectionTitle}>{t('javobgarlik')}</Text>
|
||||
<Text style={styles.text}>{t('javobgarlik_text')}</Text>
|
||||
|
||||
<Text style={styles.sectionTitle}>{t('shartlarni_ozgartirish')}</Text>
|
||||
<Text style={styles.text}>{t('shartlarni_ozgartirish_text')}</Text>
|
||||
|
||||
<Text style={styles.sectionTitle}>{t('aloqa')}</Text>
|
||||
<Text style={styles.text}>{t('aloqa_text')}</Text>
|
||||
|
||||
<View style={styles.footer}>
|
||||
<Text style={styles.footerText}>
|
||||
{t('oxirgi_yangilanish')} {new Date().toLocaleDateString('uz-UZ')}
|
||||
</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
<View style={styles.bottomContainer}>
|
||||
<TouchableOpacity style={styles.agreeButton} onPress={handleAgree}>
|
||||
<Text style={styles.agreeButtonText}>{t('roziman')}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 15,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#E5E5E5',
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: '#000',
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
color: '#000',
|
||||
marginVertical: 20,
|
||||
textAlign: 'center',
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: '#000',
|
||||
marginTop: 20,
|
||||
marginBottom: 10,
|
||||
},
|
||||
text: {
|
||||
fontSize: 14,
|
||||
lineHeight: 22,
|
||||
color: '#333',
|
||||
marginBottom: 15,
|
||||
textAlign: 'justify',
|
||||
},
|
||||
footer: {
|
||||
marginTop: 30,
|
||||
marginBottom: 20,
|
||||
alignItems: 'center',
|
||||
},
|
||||
footerText: {
|
||||
fontSize: 12,
|
||||
color: '#666',
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
bottomContainer: {
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 15,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#E5E5E5',
|
||||
},
|
||||
agreeButton: {
|
||||
backgroundColor: '#28A7E8',
|
||||
paddingVertical: 15,
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
},
|
||||
agreeButtonText: {
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
||||
export default TermsAndConditions;
|
||||
24
src/screens/auth/registeration/ui/index.tsx
Normal file
24
src/screens/auth/registeration/ui/index.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Confirm from './Confirm';
|
||||
import FirstStep from './FirstStep';
|
||||
|
||||
const Register = () => {
|
||||
const [step, setStep] = useState<number>(1);
|
||||
|
||||
const nextStep = () => {
|
||||
if (step === 1) {
|
||||
setStep(2);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{step === 1 && <FirstStep onNext={nextStep} />}
|
||||
{step === 2 && <Confirm setStep={setStep} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Register;
|
||||
210
src/screens/auth/registeration/ui/styled.ts
Normal file
210
src/screens/auth/registeration/ui/styled.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
export const RegisterStyle = StyleSheet.create({
|
||||
langContainer: {
|
||||
width: '100%',
|
||||
marginTop: 10,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
background: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
scrollContainer: {
|
||||
flexGrow: 1,
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
},
|
||||
loginContainer: {
|
||||
borderRadius: 20,
|
||||
padding: 30,
|
||||
display: 'flex',
|
||||
gap: 20,
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
},
|
||||
sectionsContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
gap: 15,
|
||||
},
|
||||
mainTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
color: '#28A7E8',
|
||||
marginBottom: 10,
|
||||
},
|
||||
label: {
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
color: '#28A7E8',
|
||||
marginBottom: 5,
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
color: '#28A7E8',
|
||||
marginBottom: 20,
|
||||
},
|
||||
errorText: {
|
||||
color: 'red',
|
||||
fontSize: 12,
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#D8DADC',
|
||||
borderRadius: 12,
|
||||
padding: 15,
|
||||
fontSize: 16,
|
||||
fontWeight: '400',
|
||||
backgroundColor: '#FFFFFF',
|
||||
color: '#000',
|
||||
},
|
||||
seriyaInput: {
|
||||
width: 60,
|
||||
fontSize: 14,
|
||||
textTransform: 'uppercase',
|
||||
borderTopRightRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
},
|
||||
raqamInput: {
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
},
|
||||
selector: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
},
|
||||
selectedText: {
|
||||
fontSize: 14,
|
||||
color: '#D8DADC',
|
||||
},
|
||||
dropdown: {
|
||||
position: 'absolute',
|
||||
top: 80,
|
||||
left: 0,
|
||||
right: 0,
|
||||
backgroundColor: '#fff',
|
||||
borderWidth: 1,
|
||||
borderColor: '#ccc',
|
||||
borderRadius: 8,
|
||||
zIndex: 10,
|
||||
elevation: 5,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 4,
|
||||
maxHeight: 150,
|
||||
},
|
||||
dropdownItem: {
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 12,
|
||||
},
|
||||
dropdownItemText: {
|
||||
fontSize: 14,
|
||||
color: '#333',
|
||||
},
|
||||
button: {
|
||||
backgroundColor: '#28A7E8',
|
||||
height: 56,
|
||||
borderRadius: 8,
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
btnText: {
|
||||
color: '#fff',
|
||||
fontSize: 20,
|
||||
textAlign: 'center',
|
||||
},
|
||||
termsContainer: {
|
||||
marginVertical: 15,
|
||||
paddingHorizontal: 5,
|
||||
},
|
||||
checkboxContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
paddingVertical: 5,
|
||||
},
|
||||
checkbox: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderWidth: 2.5,
|
||||
borderColor: '#E0E4E7',
|
||||
borderRadius: 6,
|
||||
marginRight: 12,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginTop: 1,
|
||||
backgroundColor: '#FFFFFF',
|
||||
elevation: 2,
|
||||
},
|
||||
checkboxChecked: {
|
||||
backgroundColor: '#28A7E8',
|
||||
borderColor: '#28A7E8',
|
||||
},
|
||||
termsTextContainer: {
|
||||
flex: 1,
|
||||
paddingTop: 2,
|
||||
},
|
||||
termsText: {
|
||||
fontSize: 15,
|
||||
color: '#2C3E50',
|
||||
lineHeight: 22,
|
||||
fontWeight: '400',
|
||||
},
|
||||
termsLink: {
|
||||
color: '#28A7E8',
|
||||
fontWeight: '600',
|
||||
textDecorationLine: 'underline',
|
||||
fontSize: 15,
|
||||
},
|
||||
buttonDisabled: {
|
||||
backgroundColor: '#28A7E8',
|
||||
opacity: 0.7,
|
||||
},
|
||||
buttonTextDisabled: {
|
||||
color: 'white',
|
||||
},
|
||||
btnRegister: {
|
||||
color: '#28A7E8',
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
},
|
||||
inputContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: '#D8DADC',
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 15,
|
||||
height: 56,
|
||||
backgroundColor: '#FFFFFF',
|
||||
},
|
||||
iconButton: {
|
||||
position: 'absolute',
|
||||
right: 12,
|
||||
top: '50%',
|
||||
transform: [{ translateY: -12 }],
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: 24,
|
||||
width: 24,
|
||||
},
|
||||
});
|
||||
146
src/screens/auth/select-auth/SelectAuth.tsx
Normal file
146
src/screens/auth/select-auth/SelectAuth.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Image,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
useWindowDimensions,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import Logo from 'screens/../../assets/bootsplash/logo_512.png';
|
||||
import { RootStackParamList } from 'types/types';
|
||||
|
||||
type LoginScreenNavigationProp = NativeStackNavigationProp<
|
||||
RootStackParamList,
|
||||
'Login'
|
||||
>;
|
||||
|
||||
const SelectAuth = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigation = useNavigation<LoginScreenNavigationProp>();
|
||||
const { width } = useWindowDimensions();
|
||||
|
||||
const isSmallScreen = width < 360;
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<View style={styles.container}>
|
||||
<ScrollView
|
||||
style={{ flex: 1 }}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={[styles.innerContainer, { maxWidth: 500 }]}>
|
||||
<View style={styles.logoWrapper}>
|
||||
<Image
|
||||
source={Logo}
|
||||
style={[
|
||||
styles.logoImage,
|
||||
{
|
||||
width: isSmallScreen ? 120 : 250,
|
||||
height: isSmallScreen ? 120 : 200,
|
||||
borderRadius: 20000,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Text
|
||||
style={[styles.logoText, { fontSize: isSmallScreen ? 22 : 50 }]}
|
||||
>
|
||||
CPOST
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Text style={[styles.title, { fontSize: isSmallScreen ? 20 : 24 }]}>
|
||||
{t('Ro’yxatdan o’tganmisz')}
|
||||
</Text>
|
||||
|
||||
<View style={styles.btnContainer}>
|
||||
<View style={{ gap: 4 }}>
|
||||
<Text style={styles.helperText}>
|
||||
{t("Botdan ro'yxatdan o’tganmisiz")}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
onPress={() => navigation.navigate('Login')}
|
||||
style={styles.button}
|
||||
>
|
||||
<Text style={styles.btnText}>{t('Tizimga kirish')}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={{ gap: 4 }}>
|
||||
<Text style={styles.helperText}>
|
||||
{t("Yangi ro’yxatdan o'tmoqchimisiz")}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
onPress={() => navigation.navigate('Register')}
|
||||
style={styles.button}
|
||||
>
|
||||
<Text style={styles.btnText}>{t('Ro’yxatdan o’tish')}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectAuth;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 20,
|
||||
margin: 5,
|
||||
borderRadius: 12,
|
||||
},
|
||||
scrollContent: {
|
||||
flexGrow: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
innerContainer: {
|
||||
width: '100%',
|
||||
paddingHorizontal: 10,
|
||||
},
|
||||
logoWrapper: {
|
||||
alignItems: 'center',
|
||||
marginBottom: 40,
|
||||
},
|
||||
logoImage: {
|
||||
resizeMode: 'stretch',
|
||||
},
|
||||
logoText: {
|
||||
fontWeight: '700',
|
||||
color: '#28A7E8',
|
||||
},
|
||||
title: {
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
color: '#28A7E8',
|
||||
marginBottom: 20,
|
||||
},
|
||||
helperText: {
|
||||
color: '#28A7E8',
|
||||
fontSize: 16,
|
||||
},
|
||||
button: {
|
||||
backgroundColor: '#28A7E8',
|
||||
height: 56,
|
||||
borderRadius: 6,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
btnText: {
|
||||
color: '#fff',
|
||||
fontSize: 18,
|
||||
textAlign: 'center',
|
||||
},
|
||||
btnContainer: {
|
||||
gap: 16,
|
||||
},
|
||||
});
|
||||
120
src/screens/auth/select-language/SelectLang.tsx
Normal file
120
src/screens/auth/select-language/SelectLang.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/Feather';
|
||||
import RU from 'screens/../../assets/bootsplash/RU.png';
|
||||
import UZ from 'screens/../../assets/bootsplash/UZ.png';
|
||||
import { changeLanguage } from 'utils/changeLanguage';
|
||||
|
||||
const languages = [
|
||||
{ code: 'uz', label: 'Uzb', Icon: UZ },
|
||||
{ code: 'ru', label: 'Ru', Icon: RU },
|
||||
];
|
||||
|
||||
const LanguageSelector = () => {
|
||||
const { i18n } = useTranslation();
|
||||
const [dropdownVisible, setDropdownVisible] = useState(false);
|
||||
|
||||
const selectedLang = languages.find(l => l.code === i18n.language);
|
||||
|
||||
const handleLanguageChange = async (lang: string) => {
|
||||
await changeLanguage(lang);
|
||||
setDropdownVisible(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.wrapper}>
|
||||
<TouchableOpacity
|
||||
style={styles.selector}
|
||||
onPress={() => setDropdownVisible(prev => !prev)}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: 10,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={selectedLang?.Icon}
|
||||
style={{ width: 20, height: 20, objectFit: 'contain' }}
|
||||
/>
|
||||
<Text style={styles.selectedText}>{selectedLang?.label}</Text>
|
||||
</View>
|
||||
<Icon
|
||||
name={dropdownVisible ? 'chevron-up' : 'chevron-down'}
|
||||
size={20}
|
||||
color="#555"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
{dropdownVisible && (
|
||||
<View style={styles.dropdown}>
|
||||
{languages.map(item => (
|
||||
<TouchableOpacity
|
||||
key={item.code}
|
||||
style={styles.dropdownItem}
|
||||
onPress={() => handleLanguageChange(item.code)}
|
||||
>
|
||||
<Image
|
||||
source={item.Icon}
|
||||
style={{ width: 25, height: 25, objectFit: 'contain' }}
|
||||
/>
|
||||
<Text style={styles.dropdownItemText}>{item.label}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
wrapper: {
|
||||
alignSelf: 'flex-end',
|
||||
margin: 10,
|
||||
width: 160,
|
||||
},
|
||||
selector: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#ccc',
|
||||
borderRadius: 8,
|
||||
padding: 10,
|
||||
backgroundColor: '#fff',
|
||||
flexDirection: 'row',
|
||||
gap: 10,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
alignSelf: 'flex-end',
|
||||
},
|
||||
selectedText: {
|
||||
fontSize: 14,
|
||||
color: '#333',
|
||||
},
|
||||
dropdown: {
|
||||
position: 'absolute',
|
||||
top: 45,
|
||||
right: 0,
|
||||
borderWidth: 1,
|
||||
borderColor: '#ccc',
|
||||
borderRadius: 6,
|
||||
backgroundColor: '#fff',
|
||||
zIndex: 999,
|
||||
width: 110,
|
||||
},
|
||||
dropdownItem: {
|
||||
paddingVertical: 5,
|
||||
paddingHorizontal: 5,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 5,
|
||||
},
|
||||
dropdownItemText: {
|
||||
fontSize: 14,
|
||||
color: '#333',
|
||||
},
|
||||
});
|
||||
|
||||
export default LanguageSelector;
|
||||
151
src/screens/auth/select-language/SelectLangPage.tsx
Normal file
151
src/screens/auth/select-language/SelectLangPage.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import React from 'react';
|
||||
import {
|
||||
Image,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
useWindowDimensions,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import RU from 'screens/../../assets/bootsplash/RU.png';
|
||||
import UZ from 'screens/../../assets/bootsplash/UZ.png';
|
||||
import Logo from 'screens/../../assets/bootsplash/logo_512.png';
|
||||
import { RootStackParamList } from 'types/types';
|
||||
import { changeLanguage } from 'utils/changeLanguage';
|
||||
|
||||
type NavigationProp = NativeStackNavigationProp<RootStackParamList>;
|
||||
|
||||
const SelectLangPage = () => {
|
||||
const navigation = useNavigation<NavigationProp>();
|
||||
const { width } = useWindowDimensions();
|
||||
|
||||
const isSmallScreen = width < 380;
|
||||
|
||||
const selectLanguage = async (lang: 'uz' | 'ru') => {
|
||||
await changeLanguage(lang);
|
||||
navigation.navigate('select-auth');
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<View style={styles.container}>
|
||||
<ScrollView
|
||||
style={{ flex: 1 }}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={[styles.innerContainer, { maxWidth: 500 }]}>
|
||||
<View style={styles.logoWrapper}>
|
||||
<Image
|
||||
source={Logo}
|
||||
style={[
|
||||
styles.logoImage,
|
||||
{
|
||||
width: isSmallScreen ? 120 : 250,
|
||||
height: isSmallScreen ? 120 : 200,
|
||||
borderRadius: 20000,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Text
|
||||
style={[styles.logoText, { fontSize: isSmallScreen ? 24 : 40 }]}
|
||||
>
|
||||
CPOST
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Text style={[styles.title, { fontSize: isSmallScreen ? 18 : 24 }]}>
|
||||
Tilni tanlang{' '}
|
||||
<Text
|
||||
style={[styles.title, { fontSize: isSmallScreen ? 14 : 18 }]}
|
||||
>
|
||||
(Выберите язык)
|
||||
</Text>
|
||||
</Text>
|
||||
|
||||
<View style={styles.btnContainer}>
|
||||
<TouchableOpacity
|
||||
onPress={() => selectLanguage('uz')}
|
||||
style={styles.button}
|
||||
>
|
||||
<Image source={UZ} style={styles.flag} />
|
||||
<Text style={styles.btnText}>O'zbek tili</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => selectLanguage('ru')}
|
||||
style={styles.button}
|
||||
>
|
||||
<Image source={RU} style={styles.flag} />
|
||||
<Text style={styles.btnText}>Русский язык</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectLangPage;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 20,
|
||||
margin: 5,
|
||||
borderRadius: 12,
|
||||
},
|
||||
scrollContent: {
|
||||
flexGrow: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
innerContainer: {
|
||||
width: '100%',
|
||||
paddingHorizontal: 10,
|
||||
},
|
||||
logoWrapper: {
|
||||
alignItems: 'center',
|
||||
marginBottom: 30,
|
||||
},
|
||||
logoImage: {
|
||||
resizeMode: 'stretch',
|
||||
marginBottom: 8,
|
||||
},
|
||||
logoText: {
|
||||
fontWeight: '700',
|
||||
color: '#28A7E8',
|
||||
},
|
||||
title: {
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
color: '#28A7E8',
|
||||
marginBottom: 24,
|
||||
},
|
||||
button: {
|
||||
backgroundColor: '#28A7E8',
|
||||
height: 56,
|
||||
borderRadius: 6,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
btnText: {
|
||||
color: '#fff',
|
||||
fontSize: 18,
|
||||
},
|
||||
btnContainer: {
|
||||
gap: 16,
|
||||
},
|
||||
flag: {
|
||||
width: 30,
|
||||
height: 30,
|
||||
resizeMode: 'cover',
|
||||
},
|
||||
});
|
||||
140
src/screens/authGate/ui/AuthGate.tsx
Normal file
140
src/screens/authGate/ui/AuthGate.tsx
Normal 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 noto‘g‘ri');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnlockPasswordOrPin = (input: string) => {
|
||||
if (input === password) {
|
||||
onAuthenticated();
|
||||
} else {
|
||||
Alert.alert('Xato', 'Parol noto‘g‘ri');
|
||||
}
|
||||
};
|
||||
|
||||
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' },
|
||||
});
|
||||
96
src/screens/home/branches/ui/Branches.tsx
Normal file
96
src/screens/home/branches/ui/Branches.tsx
Normal 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 ro’yxati')} />
|
||||
<NoResult message="Xatolik yuz berdi" />
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<NavbarBack title={t('Filiallar ro’yxati')} />
|
||||
<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',
|
||||
},
|
||||
});
|
||||
243
src/screens/home/branches/ui/ListBranches.tsx
Normal file
243
src/screens/home/branches/ui/ListBranches.tsx
Normal 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 ro’yxati')} />
|
||||
{!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',
|
||||
},
|
||||
});
|
||||
272
src/screens/home/cargoPrices/ui/CargoPrices.tsx
Normal file
272
src/screens/home/cargoPrices/ui/CargoPrices.tsx
Normal 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 o’sishiga 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. Doimiyko’p 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',
|
||||
},
|
||||
});
|
||||
95
src/screens/home/home/lib/data.ts
Normal file
95
src/screens/home/home/lib/data.ts
Normal 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 ko‘chasi, 12-uy',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Farg‘ona Filiali',
|
||||
latitude: 40.384213,
|
||||
longitude: 71.784287,
|
||||
address: 'Mustaqillik ko‘chasi, 45-uy',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Samarqand Filiali',
|
||||
latitude: 39.654166,
|
||||
longitude: 66.959722,
|
||||
address: 'Registon ko‘chasi, 10-uy',
|
||||
},
|
||||
];
|
||||
104
src/screens/home/home/ui/Home.tsx
Normal file
104
src/screens/home/home/ui/Home.tsx
Normal 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;
|
||||
142
src/screens/home/home/ui/Pages.tsx
Normal file
142
src/screens/home/home/ui/Pages.tsx
Normal 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 bo’ylab 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 ro’yxati')}</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;
|
||||
276
src/screens/home/home/ui/PartyCarousel.tsx
Normal file
276
src/screens/home/home/ui/PartyCarousel.tsx
Normal 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,
|
||||
},
|
||||
});
|
||||
91
src/screens/home/home/ui/Tabs.tsx
Normal file
91
src/screens/home/home/ui/Tabs.tsx
Normal 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;
|
||||
187
src/screens/home/home/ui/TabsAuto.tsx
Normal file
187
src/screens/home/home/ui/TabsAuto.tsx
Normal 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;
|
||||
195
src/screens/home/home/ui/TabsAvia.tsx
Normal file
195
src/screens/home/home/ui/TabsAvia.tsx
Normal 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;
|
||||
182
src/screens/home/home/ui/styled.ts
Normal file
182
src/screens/home/home/ui/styled.ts
Normal 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',
|
||||
},
|
||||
});
|
||||
314
src/screens/home/restrictedProduct/ui/RestrictedProduct.tsx
Normal file
314
src/screens/home/restrictedProduct/ui/RestrictedProduct.tsx
Normal 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 bo’lgan istalgan narsa')}
|
||||
</Text>
|
||||
<Text style={styles.desc}>
|
||||
{t(
|
||||
'(Telifon, sensitive buyumlar, airpods, naushnik, qo’l 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('O’tkir tig’li 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',
|
||||
},
|
||||
});
|
||||
72
src/screens/home/uncodified/ui/Uncodified.tsx
Normal file
72
src/screens/home/uncodified/ui/Uncodified.tsx
Normal 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;
|
||||
12
src/screens/passport/createPassport/lib/form.ts
Normal file
12
src/screens/passport/createPassport/lib/form.ts
Normal 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>;
|
||||
234
src/screens/passport/createPassport/ui/CreateModal.tsx
Normal file
234
src/screens/passport/createPassport/ui/CreateModal.tsx
Normal 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;
|
||||
404
src/screens/passport/createPassport/ui/CreatePassword.tsx
Normal file
404
src/screens/passport/createPassport/ui/CreatePassword.tsx
Normal 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;
|
||||
30
src/screens/passport/myPassport/lib/data.ts
Normal file
30
src/screens/passport/myPassport/lib/data.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
interface PassportData {
|
||||
id: string; // Tez ID
|
||||
jshshir: string; // JSHSHIR
|
||||
fullName: string; // To‘liq ism
|
||||
passport: string; // Pasport raqami
|
||||
birthDate: string; // Tug‘ilgan 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$',
|
||||
},
|
||||
];
|
||||
203
src/screens/passport/myPassport/ui/MyPassport.tsx
Normal file
203
src/screens/passport/myPassport/ui/MyPassport.tsx
Normal 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 ma’lumotlarim')}</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('To’liq 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('Tug’ilgan 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;
|
||||
195
src/screens/passport/myPassport/ui/Passport.tsx
Normal file
195
src/screens/passport/myPassport/ui/Passport.tsx
Normal 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;
|
||||
194
src/screens/passport/myPassport/ui/styled.ts
Normal file
194
src/screens/passport/myPassport/ui/styled.ts
Normal 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 }],
|
||||
},
|
||||
});
|
||||
99
src/screens/profile/myProfile/ui/Profile.tsx
Normal file
99
src/screens/profile/myProfile/ui/Profile.tsx
Normal 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;
|
||||
353
src/screens/profile/myProfile/ui/ProfileHeader.tsx
Normal file
353
src/screens/profile/myProfile/ui/ProfileHeader.tsx
Normal 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 ko‘rinishi
|
||||
},
|
||||
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;
|
||||
179
src/screens/profile/myProfile/ui/ProfilePages.tsx
Normal file
179
src/screens/profile/myProfile/ui/ProfilePages.tsx
Normal 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, store’ni ham ochib bo‘lmasa, 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 o‘chirish')}
|
||||
</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',
|
||||
},
|
||||
});
|
||||
38
src/screens/profile/notifications/lib/data.ts
Normal file
38
src/screens/profile/notifications/lib/data.ts
Normal 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 qo‘shildi. Buyurtma raqami: 67890.',
|
||||
// },
|
||||
// {
|
||||
// id: 3,
|
||||
// title: 'Yetkazib berish kechikdi',
|
||||
// message: 'Ob-havo sababli yukingiz kechikmoqda. Tez orada yetkaziladi.',
|
||||
// },
|
||||
// {
|
||||
// id: 4,
|
||||
// title: 'Yuk jo‘natildi',
|
||||
// message: 'Sizning 34567 raqamli yukingiz jo‘natildi va yo‘lga 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.',
|
||||
// },
|
||||
];
|
||||
121
src/screens/profile/notifications/ui/Notifications.tsx
Normal file
121
src/screens/profile/notifications/ui/Notifications.tsx
Normal 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%',
|
||||
},
|
||||
});
|
||||
186
src/screens/profile/notifications/ui/NotificationsModal.tsx
Normal file
186
src/screens/profile/notifications/ui/NotificationsModal.tsx
Normal 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;
|
||||
442
src/screens/profile/settings/ui/AddedLock.tsx
Normal file
442
src/screens/profile/settings/ui/AddedLock.tsx
Normal 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;
|
||||
181
src/screens/profile/settings/ui/Settings.tsx
Normal file
181
src/screens/profile/settings/ui/Settings.tsx
Normal 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',
|
||||
},
|
||||
});
|
||||
86
src/screens/profile/settings/ui/SettingsLock.tsx
Normal file
86
src/screens/profile/settings/ui/SettingsLock.tsx
Normal 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;
|
||||
181
src/screens/profile/support/ui/Support.tsx
Normal file
181
src/screens/profile/support/ui/Support.tsx
Normal 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',
|
||||
},
|
||||
});
|
||||
179
src/screens/profile/warehouses/ui/TabsAutoWarehouses.tsx
Normal file
179
src/screens/profile/warehouses/ui/TabsAutoWarehouses.tsx
Normal 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;
|
||||
191
src/screens/profile/warehouses/ui/TabsAviaWarehouses.tsx
Normal file
191
src/screens/profile/warehouses/ui/TabsAviaWarehouses.tsx
Normal 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;
|
||||
123
src/screens/profile/warehouses/ui/Warehouses.tsx
Normal file
123
src/screens/profile/warehouses/ui/Warehouses.tsx
Normal 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',
|
||||
},
|
||||
});
|
||||
474
src/screens/status/lib/data.ts
Normal file
474
src/screens/status/lib/data.ts
Normal 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 so’m',
|
||||
// cargo: 'auto',
|
||||
// products: [
|
||||
// {
|
||||
// trekId: 'TRK001',
|
||||
// name: 'Avto detal A1',
|
||||
// weight: '6kg',
|
||||
// quantity: 2,
|
||||
// totalPrice: '78 980 so’m',
|
||||
// },
|
||||
// {
|
||||
// trekId: 'TRK002',
|
||||
// name: 'Filtr A2',
|
||||
// weight: '3kg',
|
||||
// quantity: 3,
|
||||
// totalPrice: '39 490 so’m',
|
||||
// },
|
||||
// {
|
||||
// trekId: 'TRK002',
|
||||
// name: 'Filtr A2',
|
||||
// weight: '3kg',
|
||||
// quantity: 3,
|
||||
// totalPrice: '39 490 so’m',
|
||||
// },
|
||||
// {
|
||||
// trekId: 'TRK002',
|
||||
// name: 'Filtr A2',
|
||||
// weight: '3kg',
|
||||
// quantity: 3,
|
||||
// totalPrice: '39 490 so’m',
|
||||
// },
|
||||
// {
|
||||
// trekId: 'TRK002',
|
||||
// name: 'Filtr A2',
|
||||
// weight: '3kg',
|
||||
// quantity: 3,
|
||||
// totalPrice: '39 490 so’m',
|
||||
// },
|
||||
// {
|
||||
// trekId: 'TRK002',
|
||||
// name: 'Filtr A2',
|
||||
// weight: '3kg',
|
||||
// quantity: 3,
|
||||
// totalPrice: '39 490 so’m',
|
||||
// },
|
||||
// {
|
||||
// trekId: 'TRK002',
|
||||
// name: 'Filtr A2',
|
||||
// weight: '3kg',
|
||||
// quantity: 3,
|
||||
// totalPrice: '39 490 so’m',
|
||||
// },
|
||||
// {
|
||||
// trekId: 'TRK002',
|
||||
// name: 'Filtr A2',
|
||||
// weight: '3kg',
|
||||
// quantity: 3,
|
||||
// totalPrice: '39 490 so’m',
|
||||
// },
|
||||
// {
|
||||
// trekId: 'TRK002',
|
||||
// name: 'Filtr A2',
|
||||
// weight: '3kg',
|
||||
// quantity: 3,
|
||||
// totalPrice: '39 490 so’m',
|
||||
// },
|
||||
// {
|
||||
// trekId: 'TRK002',
|
||||
// name: 'Filtr A2',
|
||||
// weight: '3kg',
|
||||
// quantity: 3,
|
||||
// totalPrice: '39 490 so’m',
|
||||
// },
|
||||
// {
|
||||
// trekId: 'TRK002',
|
||||
// name: 'Filtr A2',
|
||||
// weight: '3kg',
|
||||
// quantity: 3,
|
||||
// totalPrice: '39 490 so’m',
|
||||
// },
|
||||
// {
|
||||
// trekId: 'TRK002',
|
||||
// name: 'Filtr A2',
|
||||
// weight: '3kg',
|
||||
// quantity: 3,
|
||||
// totalPrice: '39 490 so’m',
|
||||
// },
|
||||
// {
|
||||
// trekId: 'TRK002',
|
||||
// name: 'Filtr A2',
|
||||
// weight: '3kg',
|
||||
// quantity: 3,
|
||||
// totalPrice: '39 490 so’m',
|
||||
// },
|
||||
// {
|
||||
// trekId: 'TRK002',
|
||||
// name: 'Filtr A2',
|
||||
// weight: '3kg',
|
||||
// quantity: 3,
|
||||
// totalPrice: '39 490 so’m',
|
||||
// },
|
||||
// {
|
||||
// trekId: 'TRK002',
|
||||
// name: 'Filtr A2',
|
||||
// weight: '3kg',
|
||||
// quantity: 3,
|
||||
// totalPrice: '39 490 so’m',
|
||||
// },
|
||||
// {
|
||||
// trekId: 'TRK002',
|
||||
// name: 'Filtr A2',
|
||||
// weight: '3kg',
|
||||
// quantity: 3,
|
||||
// totalPrice: '39 490 so’m',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// status: 'way',
|
||||
// reys: 'REYS-A3',
|
||||
// weight: '22kg',
|
||||
// price: '143 450 so’m',
|
||||
// cargo: 'auto',
|
||||
// products: [
|
||||
// {
|
||||
// trekId: 'TRK003',
|
||||
// name: 'Yuk qutisi A2',
|
||||
// weight: '10kg',
|
||||
// quantity: 2,
|
||||
// totalPrice: '95 300 so’m',
|
||||
// },
|
||||
// {
|
||||
// trekId: 'TRK004',
|
||||
// name: 'Polietilen paket',
|
||||
// weight: '2kg',
|
||||
// quantity: 2,
|
||||
// totalPrice: '48 150 so’m',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// status: 'warehouse',
|
||||
// reys: 'REYS-A1',
|
||||
// weight: '18kg',
|
||||
// price: '118 470 so’m',
|
||||
// cargo: 'auto',
|
||||
// products: [
|
||||
// {
|
||||
// trekId: 'TRK001',
|
||||
// name: 'Avto detal A1',
|
||||
// weight: '6kg',
|
||||
// quantity: 2,
|
||||
// totalPrice: '78 980 so’m',
|
||||
// },
|
||||
// {
|
||||
// trekId: 'TRK002',
|
||||
// name: 'Filtr A2',
|
||||
// weight: '3kg',
|
||||
// quantity: 3,
|
||||
// totalPrice: '39 490 so’m',
|
||||
// },
|
||||
// {
|
||||
// trekId: 'TRK002',
|
||||
// name: 'Filtr A2',
|
||||
// weight: '3kg',
|
||||
// quantity: 3,
|
||||
// totalPrice: '39 490 so’m',
|
||||
// },
|
||||
// {
|
||||
// trekId: 'TRK002',
|
||||
// name: 'Filtr A2',
|
||||
// weight: '3kg',
|
||||
// quantity: 3,
|
||||
// totalPrice: '39 490 so’m',
|
||||
// },
|
||||
// {
|
||||
// trekId: 'TRK002',
|
||||
// name: 'Filtr A2',
|
||||
// weight: '3kg',
|
||||
// quantity: 3,
|
||||
// totalPrice: '39 490 so’m',
|
||||
// },
|
||||
// {
|
||||
// trekId: 'TRK002',
|
||||
// name: 'Filtr A2',
|
||||
// weight: '3kg',
|
||||
// quantity: 3,
|
||||
// totalPrice: '39 490 so’m',
|
||||
// },
|
||||
// {
|
||||
// trekId: 'TRK002',
|
||||
// name: 'Filtr A2',
|
||||
// weight: '3kg',
|
||||
// quantity: 3,
|
||||
// totalPrice: '39 490 so’m',
|
||||
// },
|
||||
// {
|
||||
// trekId: 'TRK002',
|
||||
// name: 'Filtr A2',
|
||||
// weight: '3kg',
|
||||
// quantity: 3,
|
||||
// totalPrice: '39 490 so’m',
|
||||
// },
|
||||
// {
|
||||
// trekId: 'TRK002',
|
||||
// name: 'Filtr A2',
|
||||
// weight: '3kg',
|
||||
// quantity: 3,
|
||||
// totalPrice: '39 490 so’m',
|
||||
// },
|
||||
// {
|
||||
// trekId: 'TRK002',
|
||||
// name: 'Filtr A2',
|
||||
// weight: '3kg',
|
||||
// quantity: 3,
|
||||
// totalPrice: '39 490 so’m',
|
||||
// },
|
||||
// {
|
||||
// trekId: 'TRK002',
|
||||
// name: 'Filtr A2',
|
||||
// weight: '3kg',
|
||||
// quantity: 3,
|
||||
// totalPrice: '39 490 so’m',
|
||||
// },
|
||||
// {
|
||||
// trekId: 'TRK002',
|
||||
// name: 'Filtr A2',
|
||||
// weight: '3kg',
|
||||
// quantity: 3,
|
||||
// totalPrice: '39 490 so’m',
|
||||
// },
|
||||
// {
|
||||
// trekId: 'TRK002',
|
||||
// name: 'Filtr A2',
|
||||
// weight: '3kg',
|
||||
// quantity: 3,
|
||||
// totalPrice: '39 490 so’m',
|
||||
// },
|
||||
// {
|
||||
// trekId: 'TRK002',
|
||||
// name: 'Filtr A2',
|
||||
// weight: '3kg',
|
||||
// quantity: 3,
|
||||
// totalPrice: '39 490 so’m',
|
||||
// },
|
||||
// {
|
||||
// trekId: 'TRK002',
|
||||
// name: 'Filtr A2',
|
||||
// weight: '3kg',
|
||||
// quantity: 3,
|
||||
// totalPrice: '39 490 so’m',
|
||||
// },
|
||||
// {
|
||||
// trekId: 'TRK002',
|
||||
// name: 'Filtr A2',
|
||||
// weight: '3kg',
|
||||
// quantity: 3,
|
||||
// totalPrice: '39 490 so’m',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// status: 'customs',
|
||||
// reys: 'REYS-A2',
|
||||
// weight: '28kg',
|
||||
// price: '165 000 so’m',
|
||||
// cargo: 'auto',
|
||||
// products: [
|
||||
// {
|
||||
// trekId: 'TRK005',
|
||||
// name: 'Kiyimlar A3',
|
||||
// weight: '10kg',
|
||||
// quantity: 1,
|
||||
// totalPrice: '55 000 so’m',
|
||||
// },
|
||||
// {
|
||||
// trekId: 'TRK006',
|
||||
// name: 'Sumka',
|
||||
// weight: '8kg',
|
||||
// quantity: 2,
|
||||
// totalPrice: '110 000 so’m',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// status: 'customs',
|
||||
// reys: 'REYS-A4',
|
||||
// weight: '16kg',
|
||||
// price: '97 500 so’m',
|
||||
// cargo: 'auto',
|
||||
// products: [
|
||||
// {
|
||||
// trekId: 'TRK007',
|
||||
// name: 'Elektronika V1',
|
||||
// weight: '4kg',
|
||||
// quantity: 2,
|
||||
// totalPrice: '65 000 so’m',
|
||||
// },
|
||||
// {
|
||||
// trekId: 'TRK008',
|
||||
// name: 'Adapter',
|
||||
// weight: '2kg',
|
||||
// quantity: 2,
|
||||
// totalPrice: '32 500 so’m',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// status: 'delivery',
|
||||
// reys: 'REYS-A5',
|
||||
// weight: '28kg',
|
||||
// price: '149 400 so’m',
|
||||
// cargo: 'auto',
|
||||
// products: [
|
||||
// {
|
||||
// trekId: 'TRK009',
|
||||
// name: 'Kitoblar V2',
|
||||
// weight: '5kg',
|
||||
// quantity: 2,
|
||||
// totalPrice: '71 300 so’m',
|
||||
// },
|
||||
// {
|
||||
// trekId: 'TRK010',
|
||||
// name: 'Daftar',
|
||||
// weight: '3kg',
|
||||
// quantity: 2,
|
||||
// totalPrice: '53 400 so’m',
|
||||
// },
|
||||
// {
|
||||
// trekId: 'TRK011',
|
||||
// name: 'Ruchka to‘plami',
|
||||
// weight: '2kg',
|
||||
// quantity: 2,
|
||||
// totalPrice: '17 800 so’m',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// status: 'accepted',
|
||||
// reys: 'REYS-A6',
|
||||
// weight: '30kg',
|
||||
// price: '110 000 so’m',
|
||||
// cargo: 'auto',
|
||||
// products: [
|
||||
// {
|
||||
// trekId: 'TRK012',
|
||||
// name: 'Sport anjomlari V3',
|
||||
// weight: '10kg',
|
||||
// quantity: 2,
|
||||
// totalPrice: '110 000 so’m',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
|
||||
// // AVIA
|
||||
// {
|
||||
// status: 'gathering',
|
||||
// reys: 'REYS-V1',
|
||||
// weight: '18kg',
|
||||
// price: '108 000 so’m',
|
||||
// cargo: 'avia',
|
||||
// products: [
|
||||
// {
|
||||
// trekId: 'TRK013',
|
||||
// name: 'Aksessuarlar',
|
||||
// weight: '8kg',
|
||||
// quantity: 1,
|
||||
// totalPrice: '36 000 so’m',
|
||||
// },
|
||||
// {
|
||||
// trekId: 'TRK014',
|
||||
// name: 'Soat',
|
||||
// weight: '5kg',
|
||||
// quantity: 2,
|
||||
// totalPrice: '72 000 so’m',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// status: 'way',
|
||||
// reys: 'REYS-V3',
|
||||
// weight: '12kg',
|
||||
// price: '84 000 so’m',
|
||||
// cargo: 'avia',
|
||||
// products: [
|
||||
// {
|
||||
// trekId: 'TRK015',
|
||||
// name: 'Telefon qopqog‘i',
|
||||
// weight: '6kg',
|
||||
// quantity: 2,
|
||||
// totalPrice: '42 000 so’m',
|
||||
// },
|
||||
// {
|
||||
// trekId: 'TRK016',
|
||||
// name: 'Quvvatlagich',
|
||||
// weight: '6kg',
|
||||
// quantity: 2,
|
||||
// totalPrice: '42 000 so’m',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// status: 'delivery',
|
||||
// reys: 'REYS-V5',
|
||||
// weight: '20kg',
|
||||
// price: '100 000 so’m',
|
||||
// cargo: 'avia',
|
||||
// products: [
|
||||
// {
|
||||
// trekId: 'TRK017',
|
||||
// name: 'Parfyum',
|
||||
// weight: '10kg',
|
||||
// quantity: 2,
|
||||
// totalPrice: '100 000 so’m',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// ];
|
||||
250
src/screens/status/ui/Filter.tsx
Normal file
250
src/screens/status/ui/Filter.tsx
Normal 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;
|
||||
323
src/screens/status/ui/Order.tsx
Normal file
323
src/screens/status/ui/Order.tsx
Normal 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 og’irligi')}</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;
|
||||
247
src/screens/status/ui/OrderDetailModal.tsx
Normal file
247
src/screens/status/ui/OrderDetailModal.tsx
Normal 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('Og’irligi')}: {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('so‘m')}
|
||||
</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;
|
||||
286
src/screens/status/ui/Status.tsx
Normal file
286
src/screens/status/ui/Status.tsx
Normal 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;
|
||||
144
src/screens/status/ui/Tabs.tsx
Normal file
144
src/screens/status/ui/Tabs.tsx
Normal 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;
|
||||
9
src/screens/wallet/enterCard/lib/form.ts
Normal file
9
src/screens/wallet/enterCard/lib/form.ts
Normal 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>;
|
||||
306
src/screens/wallet/enterCard/ui/EnterCard.tsx
Normal file
306
src/screens/wallet/enterCard/ui/EnterCard.tsx
Normal 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 qo‘shish
|
||||
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;
|
||||
45
src/screens/wallet/payment/lib/data.ts
Normal file
45
src/screens/wallet/payment/lib/data.ts
Normal 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 so’m',
|
||||
status: 'unpaid',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
reys: 'Avia-CP-23',
|
||||
weight: '8kg',
|
||||
count: 3,
|
||||
totalPrice: '52 000 so’m',
|
||||
status: 'paid',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
reys: 'Avia-CP-24',
|
||||
weight: '15kg',
|
||||
count: 7,
|
||||
totalPrice: '100 500 so’m',
|
||||
status: 'paid',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
reys: 'Avia-CP-25',
|
||||
weight: '12kg',
|
||||
count: 6,
|
||||
totalPrice: '88 200 so’m',
|
||||
status: 'paid',
|
||||
},
|
||||
];
|
||||
116
src/screens/wallet/payment/ui/Payment.tsx
Normal file
116
src/screens/wallet/payment/ui/Payment.tsx
Normal 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 to‘liq 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;
|
||||
268
src/screens/wallet/payment/ui/Wallet.tsx
Normal file
268
src/screens/wallet/payment/ui/Wallet.tsx
Normal 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;
|
||||
268
src/screens/wallet/payment/ui/style.ts
Normal file
268
src/screens/wallet/payment/ui/style.ts
Normal 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',
|
||||
},
|
||||
});
|
||||
249
src/screens/wallet/paymentMethod/ui/ModalCard.tsx
Normal file
249
src/screens/wallet/paymentMethod/ui/ModalCard.tsx
Normal 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 bo‘lsa 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;
|
||||
261
src/screens/wallet/paymentMethod/ui/ModalPay.tsx
Normal file
261
src/screens/wallet/paymentMethod/ui/ModalPay.tsx
Normal 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,
|
||||
},
|
||||
});
|
||||
246
src/screens/wallet/paymentMethod/ui/ModalSuccess.tsx
Normal file
246
src/screens/wallet/paymentMethod/ui/ModalSuccess.tsx
Normal 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('To’lov muvaffaqqiyatli o’tdi')
|
||||
: 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;
|
||||
81
src/screens/wallet/paymentMethod/ui/PaymentMethod.tsx
Normal file
81
src/screens/wallet/paymentMethod/ui/PaymentMethod.tsx
Normal 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
Reference in New Issue
Block a user