Initial commit
This commit is contained in:
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;
|
||||
Reference in New Issue
Block a user