Initial commit

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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