Compare commits

...

7 Commits

Author SHA1 Message Date
Samandar Turgunboyev
fee9213c59 get discrit token 2026-03-19 14:04:41 +05:00
Samandar Turgunboyev
22c1688781 register update 2026-03-18 17:56:26 +05:00
Samandar Turgunboyev
a34cf75c57 bug fixed 2026-03-05 11:49:56 +05:00
Samandar Turgunboyev
382b214e3e register form refferal update 2026-03-02 16:28:00 +05:00
Samandar Turgunboyev
c71651ec4b keyboard scroll 2026-03-02 15:46:12 +05:00
Samandar Turgunboyev
4d5cc84850 bug fixed complated 2026-03-02 15:14:52 +05:00
Samandar Turgunboyev
ab363ca3b9 bug fixed 2026-03-02 13:22:55 +05:00
67 changed files with 1335 additions and 1527 deletions

View File

@@ -28,7 +28,7 @@ export const API_URLS = {
User_Update: 'auth/user-update/', User_Update: 'auth/user-update/',
Employee_List: 'api/employee/', Employee_List: 'api/employee/',
My_Ads: 'api/my-ads/', My_Ads: 'api/my-ads/',
My_Ads_Detail: (id: number) => `api/my-ads/${id}`, Ads_Detail: (id: number) => `api/my-ads/${id}/`,
My_Bonuses: 'api/cashback/', My_Bonuses: 'api/cashback/',
My_Refferals: 'api/referral/', My_Refferals: 'api/referral/',
Goverment_Service: '/api/goverment-service/', Goverment_Service: '/api/goverment-service/',
@@ -37,5 +37,6 @@ export const API_URLS = {
Notification_Ready: (id: number) => `/api/notifications/${id}/read/`, Notification_Ready: (id: number) => `/api/notifications/${id}/read/`,
Notification_Mark_All_Read: '/api/notifications/read-all/', Notification_Mark_All_Read: '/api/notifications/read-all/',
Info: "/auth/get-person/", Info: "/auth/get-person/",
Get_Director_Info: "/auth/get-director/" Get_Director_Info: "/auth/get-director/",
GetTokens: "/api/tokens/",
}; };

View File

@@ -82,7 +82,6 @@ httpClient.interceptors.response.use(
// Original so'rovni qayta yuboramiz // Original so'rovni qayta yuboramiz
return httpClient(originalRequest); return httpClient(originalRequest);
} catch (refreshError) { } catch (refreshError) {
console.error('Refresh token xatosi:', refreshError);
// Refresh muvaffaqiyatsiz bo'lsa // Refresh muvaffaqiyatsiz bo'lsa
processQueue(refreshError); processQueue(refreshError);

View File

@@ -2,7 +2,7 @@
"expo": { "expo": {
"name": "Info target", "name": "Info target",
"slug": "infotarget", "slug": "infotarget",
"version": "1.0.2", "version": "1.0.3",
"orientation": "portrait", "orientation": "portrait",
"icon": "./assets/images/logo.png", "icon": "./assets/images/logo.png",
"scheme": "infotarget", "scheme": "infotarget",
@@ -11,12 +11,16 @@
"ios": { "ios": {
"supportsTablet": true, "supportsTablet": true,
"infoPlist": { "infoPlist": {
"UIBackgroundModes": ["remote-notification"] "UIBackgroundModes": [
"remote-notification"
],
"UIViewControllerBasedStatusBarAppearance": true
}, },
"bundleIdentifier": "com.felix.infotarget" "bundleIdentifier": "com.felix.infotarget"
}, },
"android": { "android": {
"useNextNotificationsApi": true, "useNextNotificationsApi": true,
"softwareKeyboardLayoutMode": "resize",
"adaptiveIcon": { "adaptiveIcon": {
"backgroundColor": "#E6F4FE", "backgroundColor": "#E6F4FE",
"foregroundImage": "./assets/images/logo.png" "foregroundImage": "./assets/images/logo.png"

View File

@@ -1,247 +1,8 @@
import AuthHeader from '@/components/ui/AuthHeader'; import RegisterFormScreen from '@/screens/auth/register/RegisterForm';
import { PersonType, useRegister } from '@/screens/auth/register/lib/useRegisterStore';
import { LinearGradient } from 'expo-linear-gradient';
import { useRouter } from 'expo-router';
import { Building2, ChevronRight, ShieldCheck, User } from 'lucide-react-native';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next';
import {
Animated,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
const PERSON_TYPES: {
key: PersonType;
label: string;
description: string;
icon: React.ReactNode;
gradient: [string, string];
}[] = [
{
key: 'legal_entity',
label: 'Yuridik shaxs',
description: "Tashkilot yoki korxona",
icon: <Building2 size={28} color="#fff" />,
gradient: ['#f59e0b', '#d97706'],
},
{
key: 'yatt',
label: 'YATT',
description: "Yakka tartibdagi tadbirkor",
icon: <User size={28} color="#fff" />,
gradient: ['#10b981', '#059669'],
},
{
key: 'band',
label: "O'zini o'zi band qilgan",
description: "O'z faoliyatini mustaqil yurituvchi",
icon: <ShieldCheck size={28} color="#fff" />,
gradient: ['#3b82f6', '#2563eb'],
},
];
function TypeCard({
item,
index,
onPress,
}: {
item: (typeof PERSON_TYPES)[number];
index: number;
onPress: () => void;
}) {
const scale = React.useRef(new Animated.Value(1)).current;
const { t } = useTranslation()
const handlePressIn = () => {
Animated.spring(scale, {
toValue: 0.96,
useNativeDriver: true,
}).start();
};
const handlePressOut = () => {
Animated.spring(scale, {
toValue: 1,
friction: 4,
useNativeDriver: true,
}).start();
};
export default function Index() {
return ( return (
<Animated.View style={{ transform: [{ scale }], marginBottom: 14 }}> <RegisterFormScreen />
<TouchableOpacity
style={styles.card}
onPress={onPress}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
activeOpacity={1}
testID={`person-type-${item.key}`}
>
<LinearGradient
colors={item.gradient}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.cardIcon}
>
{item.icon}
</LinearGradient>
<View style={styles.cardContent}>
<Text style={styles.cardTitle}>{t(item.label)}</Text>
<Text style={styles.cardDescription}>{t(item.description)}</Text>
</View>
<ChevronRight size={20} color="#94a3b8" />
</TouchableOpacity>
</Animated.View>
); );
} }
export default function PersonTypeScreen() {
const router = useRouter();
const { t } = useTranslation();
const { setPersonType, reset } = useRegister();
const handleSelect = (type: PersonType) => {
setPersonType(type);
router.push('/(auth)/register-form');
};
return (
<View style={styles.container}>
<LinearGradient
colors={['#0f172a', '#1e293b', '#334155']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={StyleSheet.absoluteFill}
/>
<View style={styles.decorCircle1} />
<View style={styles.decorCircle2} />
<AuthHeader />
<SafeAreaView style={styles.safeArea}>
<View style={styles.header}>
<Text style={styles.title}>{t("Ro'yxatdan o'tish")}</Text>
<Text style={styles.subtitle}>
{t("Faoliyat turingizni tanlang")}
</Text>
</View>
<ScrollView style={styles.cardList} showsVerticalScrollIndicator={false}>
{PERSON_TYPES.map((item, index) => (
<TypeCard
key={item.key}
item={item}
index={index}
onPress={() => { handleSelect(item.key); reset() }}
/>
))}
</ScrollView>
<View style={styles.footer}>
<TouchableOpacity onPress={() => router.back()} testID="go-to-login">
<Text style={styles.footerText}>
{t("Hisobingiz bormi?")} <Text style={styles.footerLink}>{t("Kirish")}</Text>
</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#0f172a',
},
safeArea: {
flex: 1,
paddingHorizontal: 14,
},
header: {
alignItems: 'center',
marginBottom: 20,
marginTop: 20,
},
title: {
fontSize: 28,
fontWeight: '800' as const,
color: '#ffffff',
marginBottom: 10,
letterSpacing: 0.5,
},
subtitle: {
fontSize: 15,
color: '#94a3b8',
textAlign: 'center',
lineHeight: 22,
},
cardList: {
marginBottom: 14,
},
card: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'rgba(255, 255, 255, 0.06)',
borderRadius: 20,
padding: 18,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.1)',
gap: 16,
},
cardIcon: {
width: 56,
height: 56,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
},
cardContent: {
flex: 1,
},
cardTitle: {
fontSize: 17,
fontWeight: '700' as const,
color: '#ffffff',
marginBottom: 4,
},
cardDescription: {
fontSize: 13,
color: '#94a3b8',
lineHeight: 18,
},
footer: {
marginTop: 'auto' as const,
paddingBottom: 20,
alignItems: 'center',
},
footerText: {
color: '#94a3b8',
fontSize: 14,
},
footerLink: {
color: '#3b82f6',
fontWeight: '700' as const,
},
decorCircle1: {
position: 'absolute',
top: -150,
right: -100,
width: 400,
height: 400,
borderRadius: 200,
backgroundColor: 'rgba(59, 130, 246, 0.1)',
},
decorCircle2: {
position: 'absolute',
bottom: -100,
left: -150,
width: 350,
height: 350,
borderRadius: 175,
backgroundColor: 'rgba(16, 185, 129, 0.08)',
},
});

View File

@@ -11,13 +11,13 @@ import React, { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
ActivityIndicator, ActivityIndicator,
Alert,
ScrollView, ScrollView,
StyleSheet, StyleSheet,
Text, Text,
TouchableOpacity, TouchableOpacity,
View, View
} from 'react-native'; } from 'react-native';
import { Toast } from 'toastify-react-native';
interface Category { interface Category {
id: number; id: number;
@@ -28,15 +28,17 @@ interface Category {
export default function CategorySelectScreen() { export default function CategorySelectScreen() {
const router = useRouter(); const router = useRouter();
const { t } = useTranslation(); const { t } = useTranslation();
const { phone, stir, person_type, director_full_name, referal, first_name, last_name, middle_name } = useLocalSearchParams<{ const { phone, stir, person_type, referal, director_full_name, first_name, last_name, district, company_name, address } = useLocalSearchParams<{
phone: string; phone: string;
stir: string; stir: string;
person_type: 'band' | 'ytt'; person_type: 'band' | 'ytt' | "legal_entity";
referal: string; referal: string;
director_full_name: string; director_full_name: string;
first_name: string; first_name: string;
last_name: string; last_name: string;
middle_name: string; district: string
address: string;
company_name: string;
}>(); }>();
const [selected, setSelected] = useState<number | null>(null); const [selected, setSelected] = useState<number | null>(null);
@@ -69,22 +71,32 @@ export default function CategorySelectScreen() {
person_type: string; person_type: string;
activate_types: number[]; activate_types: number[];
director_full_name: string; director_full_name: string;
referal: string; referral: string;
first_name: string; first_name: string;
last_name: string; last_name: string;
district: number;
company_name: string;
address: number;
}) => auth_api.register(body), }) => auth_api.register(body),
onSuccess: async () => { onSuccess: async () => {
router.replace('/(auth)/register-confirm'); router.replace('/(auth)/register-confirm');
await AsyncStorage.setItem('phone', phone); await AsyncStorage.setItem('phone', phone);
}, },
onError: (err: AxiosError) => { onError: (err: AxiosError) => {
const errMessage = (err.response?.data as { data: { stir: string[] } }).data.stir[0]; const errMessage = (err.response?.data as any)?.data?.stir?.[0];
const errMessageDetail = (err.response?.data as { data: { detail: string } }).data.detail; const errMessageDetail = (err.response?.data as any)?.data?.detail;
const errMessageReffral = (err.response?.data as any).data.referral[0];
const errMessageDetailData = (err.response?.data as any)?.data;
const errrAlert = errMessage ? errMessage : errMessageDetail; const message =
errMessage ||
errMessageReffral ||
errMessageDetail ||
errMessageDetailData ||
t('Xatolik yuz berdi');
Alert.alert(t('Xatolik yuz berdi'), errMessage || errrAlert || t('erroXatolik yuz berdi')); Toast.error(String(message));
}, }
}); });
const onCategoryPress = (cat: Category) => { const onCategoryPress = (cat: Category) => {
@@ -103,8 +115,6 @@ export default function CategorySelectScreen() {
setSelected(null); setSelected(null);
}; };
const full_name = first_name.length > 0 ? first_name + ' ' + last_name + ' ' + middle_name : director_full_name;
return ( return (
<View style={styles.safeArea}> <View style={styles.safeArea}>
<AuthHeader /> <AuthHeader />
@@ -149,10 +159,13 @@ export default function CategorySelectScreen() {
person_type, person_type,
phone: `998${phone}`, phone: `998${phone}`,
stir, stir,
referal: referal, referral: referal,
director_full_name: director_full_name, director_full_name,
first_name: full_name, first_name,
last_name: last_name, last_name,
district: Number(district),
address: Number(address),
company_name
}); });
}} }}
> >
@@ -179,7 +192,7 @@ const styles = StyleSheet.create({
container: { container: {
paddingHorizontal: 20, paddingHorizontal: 20,
paddingTop: 16, paddingTop: 16,
paddingBottom: 70, paddingBottom: 90,
gap: 12, gap: 12,
}, },

View File

@@ -5,9 +5,9 @@ import { useHomeStore } from '@/screens/home/lib/hook';
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet'; import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
import { router, Tabs } from 'expo-router'; import { router, Tabs } from 'expo-router';
import { Home, Megaphone, PlusCircle, User } from 'lucide-react-native'; import { Home, Megaphone, PlusCircle, User } from 'lucide-react-native';
import { useEffect, useRef } from 'react'; import { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Animated, Easing, Platform, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { Animated, Easing, Keyboard, Platform, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { GestureHandlerRootView } from 'react-native-gesture-handler';
export default function TabsLayout() { export default function TabsLayout() {
@@ -15,6 +15,17 @@ export default function TabsLayout() {
const { t } = useTranslation(); const { t } = useTranslation();
const { setShowFilter, setStep } = useHomeStore(); const { setShowFilter, setStep } = useHomeStore();
const rotateAnim = useRef(new Animated.Value(0)).current; const rotateAnim = useRef(new Animated.Value(0)).current;
const [keyboard, setKeyboard] = useState(false);
useEffect(() => {
const showSub = Keyboard.addListener('keyboardDidShow', () => setKeyboard(true));
const hideSub = Keyboard.addListener('keyboardDidHide', () => setKeyboard(false));
return () => {
showSub.remove();
hideSub.remove();
};
}, []);
useEffect(() => { useEffect(() => {
Animated.loop( Animated.loop(
@@ -52,9 +63,10 @@ export default function TabsLayout() {
headerShadowVisible: false, headerShadowVisible: false,
tabBarStyle: { tabBarStyle: {
position: 'absolute', position: 'absolute',
display: keyboard ? "none" : "flex",
left: 16, left: 16,
right: 16, right: 16,
bottom: 4, bottom: 8,
height: 70, height: 70,
paddingTop: 8, paddingTop: 8,
paddingBottom: 12, paddingBottom: 12,

View File

@@ -1,10 +1,8 @@
import { useTheme } from '@/components/ThemeContext';
import { FilterProvider } from '@/components/ui/FilterContext'; import { FilterProvider } from '@/components/ui/FilterContext';
import { CustomHeader } from '@/components/ui/Header'; import { CustomHeader } from '@/components/ui/Header';
import CreateAdsScreens from '@/screens/create-ads/ui/CreateAdsScreens'; import CreateAdsScreens from '@/screens/create-ads/ui/CreateAdsScreens';
export default function CreateAnnouncements() { export default function CreateAnnouncements() {
const { isDark } = useTheme();
return ( return (
<FilterProvider> <FilterProvider>
<CustomHeader /> <CustomHeader />

View File

@@ -7,34 +7,21 @@ import { ProfileDataProvider } from '@/screens/profile/lib/ProfileDataContext';
import { Stack } from 'expo-router'; import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar'; import { StatusBar } from 'expo-status-bar';
import { I18nextProvider } from 'react-i18next'; import { I18nextProvider } from 'react-i18next';
import { View } from 'react-native';
import 'react-native-reanimated'; import 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import ToastManager from "toastify-react-native";
function AppContent() { function AppContent() {
useNotifications(); useNotifications();
const insets = useSafeAreaInsets();
return ( return (
<> <SafeAreaView style={{ flex: 1, backgroundColor: "#000" }}>
{/* iOS status bar fon */} <StatusBar style='light' backgroundColor='#000' />
<View
style={{
height: insets.top,
backgroundColor: '#000',
}}
/>
{/* StatusBar */}
<StatusBar
style="light"
backgroundColor="#000" // Android
/>
<Stack screenOptions={{ headerShown: false }} /> <Stack screenOptions={{ headerShown: false }} />
</> </SafeAreaView>
); );
} }
export default function RootLayout() { export default function RootLayout() {
return ( return (
<I18nextProvider i18n={i18n}> <I18nextProvider i18n={i18n}>
@@ -43,6 +30,7 @@ export default function RootLayout() {
<ProfileDataProvider> <ProfileDataProvider>
<AuthProvider> <AuthProvider>
<AppContent /> <AppContent />
<ToastManager />
</AuthProvider> </AuthProvider>
</ProfileDataProvider> </ProfileDataProvider>
</ThemeProvider> </ThemeProvider>

View File

@@ -14,10 +14,10 @@ import {
ScrollView, ScrollView,
StyleSheet, StyleSheet,
Text, Text,
ToastAndroid,
TouchableOpacity, TouchableOpacity,
View, View,
} from 'react-native'; } from 'react-native';
import { Toast } from 'toastify-react-native';
export default function PersonalInfoScreen() { export default function PersonalInfoScreen() {
const router = useRouter(); const router = useRouter();
@@ -76,7 +76,7 @@ export default function PersonalInfoScreen() {
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['get_me'] }); queryClient.invalidateQueries({ queryKey: ['get_me'] });
router.push('/profile/personal-info'); router.push('/profile/personal-info');
ToastAndroid.show(t("Ma'lumotlar yangilandi"), ToastAndroid.TOP); Toast.success(t("Ma'lumotlar yangilandi"));
}, },
onError: () => { onError: () => {
Alert.alert(t('Xatolik yzu berdi'), t("Ma'lumotlarni yangilashda xatolik yuz berdi")); Alert.alert(t('Xatolik yzu berdi'), t("Ma'lumotlarni yangilashda xatolik yuz berdi"));

View File

@@ -9,17 +9,16 @@ import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
ActivityIndicator, ActivityIndicator,
Alert,
Image, Image,
Pressable, Pressable,
ScrollView, ScrollView,
StyleSheet, StyleSheet,
Text, Text,
TextInput, TextInput,
ToastAndroid,
TouchableOpacity, TouchableOpacity,
View, View
} from 'react-native'; } from 'react-native';
import { Toast } from 'toastify-react-native';
export default function PersonalInfoScreen() { export default function PersonalInfoScreen() {
const router = useRouter(); const router = useRouter();
@@ -71,10 +70,10 @@ export default function PersonalInfoScreen() {
queryClient.invalidateQueries({ queryKey: ['get_me'] }); queryClient.invalidateQueries({ queryKey: ['get_me'] });
setIsEditing(false); setIsEditing(false);
setShowCategories(false); setShowCategories(false);
ToastAndroid.show(t("Ma'lumotlar yangilandi"), ToastAndroid.TOP); Toast.success(t("Ma'lumotlar yangilandi"));
}, },
onError: () => { onError: () => {
Alert.alert(t('Xatolik yuz berdi'), t('Yangilashda xatolik yuz berdi')); Toast.error(t('Yangilashda xatolik yuz berdi'));
}, },
}); });

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,4 +1,5 @@
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
import { router } from 'expo-router';
import { createContext, useContext, useEffect, useState } from 'react'; import { createContext, useContext, useEffect, useState } from 'react';
type AuthContextType = { type AuthContextType = {
@@ -32,6 +33,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
await AsyncStorage.removeItem('access_token'); await AsyncStorage.removeItem('access_token');
await AsyncStorage.removeItem('refresh_token'); await AsyncStorage.removeItem('refresh_token');
setIsAuthenticated(false); setIsAuthenticated(false);
router.replace('/(auth)');
}; };
return ( return (

View File

@@ -34,7 +34,6 @@ export async function registerForPushNotificationsAsync() {
} }
if (finalStatus !== 'granted') { if (finalStatus !== 'granted') {
console.log('Notification uchun ruxsat berilmadi!');
return; return;
} }
@@ -43,10 +42,6 @@ export async function registerForPushNotificationsAsync() {
projectId: '67d5a024-4eb7-44ec-8b18-6c9187bd1862', projectId: '67d5a024-4eb7-44ec-8b18-6c9187bd1862',
}) })
).data; ).data;
console.log('Push Token:', token);
} else {
console.log('Push notification faqat real qurilmalarda ishlaydi!');
} }
return token; return token;

View File

@@ -1,9 +1,9 @@
import { useTheme } from '@/components/ThemeContext'; import { useTheme } from '@/components/ThemeContext';
import { products_api } from '@/screens/home/lib/api'; import { products_api } from '@/screens/home/lib/api';
import { useMutation, useQuery } from '@tanstack/react-query'; import { useMutation, useQuery } from '@tanstack/react-query';
import { AxiosError } from 'axios';
import { ChevronLeft, ChevronRight } from 'lucide-react-native'; import { ChevronLeft, ChevronRight } from 'lucide-react-native';
import React, { Dispatch, SetStateAction, useState } from 'react'; import React, { Dispatch, SetStateAction, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { import {
ActivityIndicator, ActivityIndicator,
FlatList, FlatList,
@@ -12,6 +12,7 @@ import {
TouchableOpacity, TouchableOpacity,
View, View,
} from 'react-native'; } from 'react-native';
import { Toast } from 'toastify-react-native';
interface Category { interface Category {
id: number; id: number;
@@ -44,6 +45,7 @@ export default function CategorySelect({ selectedCategories, setSelectedCategori
const [currentCategories, setCurrentCategories] = useState<Category[]>([]); const [currentCategories, setCurrentCategories] = useState<Category[]>([]);
const [currentParentId, setCurrentParentId] = useState<number | null>(null); const [currentParentId, setCurrentParentId] = useState<number | null>(null);
const [history, setHistory] = useState<HistoryItem[]>([]); const [history, setHistory] = useState<HistoryItem[]>([]);
const { t } = useTranslation()
// Root categories // Root categories
const { isLoading: rootLoading, error: rootError } = useQuery<CategoryResponse>({ const { isLoading: rootLoading, error: rootError } = useQuery<CategoryResponse>({
@@ -66,8 +68,8 @@ export default function CategorySelect({ selectedCategories, setSelectedCategori
setCurrentCategories(childCategories); setCurrentCategories(childCategories);
setCurrentParentId(id); setCurrentParentId(id);
}, },
onError: (err: AxiosError) => { onError: () => {
console.error('Child category loading error:', err); Toast.error(t("Xatolik yuz berdi"))
}, },
}); });

View File

@@ -6,7 +6,6 @@ import BottomSheet, {
BottomSheetScrollView, BottomSheetScrollView,
} from '@gorhom/bottom-sheet'; } from '@gorhom/bottom-sheet';
import { useMutation, useQuery } from '@tanstack/react-query'; import { useMutation, useQuery } from '@tanstack/react-query';
import { AxiosError } from 'axios';
import { Image } from 'expo-image'; import { Image } from 'expo-image';
import { CheckIcon, ChevronRight, XIcon } from 'lucide-react-native'; import { CheckIcon, ChevronRight, XIcon } from 'lucide-react-native';
import React, { Dispatch, SetStateAction, useCallback, useMemo, useRef, useState } from 'react'; import React, { Dispatch, SetStateAction, useCallback, useMemo, useRef, useState } from 'react';
@@ -75,7 +74,6 @@ export default function FilterUI({ back, onApply, setStep, setFiltered }: Filter
setStep('items'); setStep('items');
setFiltered(data.data.data.results); setFiltered(data.data.data.results);
}, },
onError: (error: AxiosError) => console.log(error),
}); });
const handleApply = () => { const handleApply = () => {

View File

@@ -1,8 +1,8 @@
import { products_api } from '@/screens/home/lib/api'; import { products_api } from '@/screens/home/lib/api';
import { useMutation, useQuery } from '@tanstack/react-query'; import { useMutation, useQuery } from '@tanstack/react-query';
import { AxiosError } from 'axios';
import { ChevronLeft, ChevronRight } from 'lucide-react-native'; import { ChevronLeft, ChevronRight } from 'lucide-react-native';
import React, { Dispatch, SetStateAction, useState } from 'react'; import React, { Dispatch, SetStateAction, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { import {
ActivityIndicator, ActivityIndicator,
FlatList, FlatList,
@@ -11,6 +11,7 @@ import {
TouchableOpacity, TouchableOpacity,
View, View,
} from 'react-native'; } from 'react-native';
import { Toast } from 'toastify-react-native';
import { useTheme } from '../ThemeContext'; import { useTheme } from '../ThemeContext';
interface Category { interface Category {
@@ -39,6 +40,7 @@ export default function CategorySelection({ selectedCategories, setSelectedCateg
const [history, setHistory] = useState<{ parentId: number | null; categories: Category[] }[]>([]); const [history, setHistory] = useState<{ parentId: number | null; categories: Category[] }[]>([]);
const [currentParentId, setCurrentParentId] = useState<number | null>(null); const [currentParentId, setCurrentParentId] = useState<number | null>(null);
const { isDark } = useTheme(); const { isDark } = useTheme();
const { t } = useTranslation()
const theme = { const theme = {
cardBg: isDark ? '#1e293b' : '#f8fafc', cardBg: isDark ? '#1e293b' : '#f8fafc',
@@ -75,8 +77,8 @@ export default function CategorySelection({ selectedCategories, setSelectedCateg
setCurrentCategories(childCategories); setCurrentCategories(childCategories);
setCurrentParentId(id); setCurrentParentId(id);
}, },
onError: (err: AxiosError) => { onError: () => {
console.error('Child yuklashda xato:', err); Toast.error(t("Xatolik yuz berdi"))
}, },
}); });

View File

@@ -1,8 +1,9 @@
import { useTheme } from '@/components/ThemeContext'; import { useTheme } from '@/components/ThemeContext';
import { products_api } from '@/screens/home/lib/api'; import { products_api } from '@/screens/home/lib/api';
import { ProductResponse } from '@/screens/home/lib/types'; import { ProductResponse } from '@/screens/home/lib/types';
import { user_api } from '@/screens/profile/lib/api';
import { BottomSheetBackdrop, BottomSheetModal, BottomSheetScrollView } from '@gorhom/bottom-sheet'; import { BottomSheetBackdrop, BottomSheetModal, BottomSheetScrollView } from '@gorhom/bottom-sheet';
import { useInfiniteQuery } from '@tanstack/react-query'; import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
import { ResizeMode, Video } from 'expo-av'; import { ResizeMode, Video } from 'expo-av';
import { Info, Package, PlayCircle } from 'lucide-react-native'; import { Info, Package, PlayCircle } from 'lucide-react-native';
import React, { useCallback, useRef, useState } from 'react'; import React, { useCallback, useRef, useState } from 'react';
@@ -50,6 +51,17 @@ export default function ProductList({ query }: Props) {
const allProducts = data?.pages.flatMap((p) => p.results) ?? []; const allProducts = data?.pages.flatMap((p) => p.results) ?? [];
const {
data: detail,
isLoading: loadingDetail,
isError: detailError,
} = useQuery({
queryKey: ['my_ads_id', selectedProduct],
queryFn: () => user_api.detail_service(Number(selectedProduct?.id)),
select: (res) => res.data.data,
enabled: !!selectedProduct,
});
const handlePresentModalPress = useCallback((product: ProductResponse) => { const handlePresentModalPress = useCallback((product: ProductResponse) => {
setSelectedProduct(product); setSelectedProduct(product);
setCurrentImageIndex(0); setCurrentImageIndex(0);
@@ -181,12 +193,29 @@ export default function ProductList({ query }: Props) {
style={styles.sheetContent} style={styles.sheetContent}
contentContainerStyle={styles.sheetContentContainer} contentContainerStyle={styles.sheetContentContainer}
> >
{selectedProduct && ( {/* Loading holati */}
{loadingDetail && (
<View style={styles.center}>
<ActivityIndicator size="large" color="#3b82f6" />
</View>
)}
{/* Error holati */}
{detailError && (
<View style={styles.center}>
<Text style={{ color: '#ef4444', fontWeight: '600' }}>
{t('Xatolik yuz berdi')}
</Text>
</View>
)}
{/* Detail mavjud bolsa */}
{detail && (
<> <>
<View style={styles.carouselWrapper}> <View style={styles.carouselWrapper}>
<FlatList <FlatList
nestedScrollEnabled={true} nestedScrollEnabled={true}
data={selectedProduct.files || []} data={detail.files || []}
renderItem={renderCarouselItem} renderItem={renderCarouselItem}
keyExtractor={(item) => item.id.toString()} keyExtractor={(item) => item.id.toString()}
horizontal horizontal
@@ -197,9 +226,9 @@ export default function ProductList({ query }: Props) {
setCurrentImageIndex(index); setCurrentImageIndex(index);
}} }}
/> />
{selectedProduct.files.length > 1 && ( {detail.files.length > 1 && (
<View style={styles.pagination}> <View style={styles.pagination}>
{selectedProduct.files.map((_, i) => ( {detail.files.map((_, i) => (
<View <View
key={i} key={i}
style={[ style={[
@@ -214,10 +243,10 @@ export default function ProductList({ query }: Props) {
<View style={styles.sheetHeader}> <View style={styles.sheetHeader}>
<Text style={[styles.sheetTitle, isDark ? styles.darkText : styles.lightText]}> <Text style={[styles.sheetTitle, isDark ? styles.darkText : styles.lightText]}>
{selectedProduct.title} {detail.title}
</Text> </Text>
<View style={styles.sheetCompanyBadge}> <View style={styles.sheetCompanyBadge}>
<Text style={styles.sheetCompanyText}>{selectedProduct.company}</Text> <Text style={styles.sheetCompanyText}>{detail.company}</Text>
</View> </View>
</View> </View>
@@ -230,10 +259,8 @@ export default function ProductList({ query }: Props) {
{t("Batafsil ma'lumot")} {t("Batafsil ma'lumot")}
</Text> </Text>
</View> </View>
<Text <Text style={[styles.sheetDescription, isDark ? styles.darkText : styles.lightText]}>
style={[styles.sheetDescription, isDark ? styles.darkText : styles.lightText]} {detail.description || "Ma'lumot mavjud emas."}
>
{selectedProduct.description || "Ma'lumot mavjud emas."}
</Text> </Text>
</View> </View>
</> </>
@@ -245,7 +272,6 @@ export default function ProductList({ query }: Props) {
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
listContainer: { gap: 0, paddingBottom: 20 },
card: { card: {
borderRadius: 16, borderRadius: 16,
overflow: 'hidden', overflow: 'hidden',

View File

@@ -18,9 +18,7 @@ export function RefreshProvider({ children }: { children: React.ReactNode }) {
setRefreshing(true); setRefreshing(true);
try { try {
await queryClient.refetchQueries(); await queryClient.refetchQueries();
} catch (err) { } catch (err) { } finally {
console.error('Global refresh error:', err);
} finally {
setRefreshing(false); setRefreshing(false);
} }
}, [queryClient, refreshing]); }, [queryClient, refreshing]);

View File

@@ -13,7 +13,5 @@ export const script = (mode: string) => {
documentElement.classList.remove(theme === 'light' ? 'dark' : 'light'); documentElement.classList.remove(theme === 'light' ? 'dark' : 'light');
documentElement.classList.add(theme); documentElement.classList.add(theme);
documentElement.style.colorScheme = theme; documentElement.style.colorScheme = theme;
} catch (e) { } catch (e) { }
console.error(e);
}
}; };

41
constants/crypto.ts Normal file
View File

@@ -0,0 +1,41 @@
import CryptoJS from 'crypto-js';
/**
* Backenddan kelgan shifrlangan tokenni ochish
*/
export const decryptToken = (encryptedBase64: string) => {
try {
// 1. Base64 dan WordArray ga
const encryptedData = CryptoJS.enc.Base64.parse(encryptedBase64);
// 2. IV (dastlabki 16 bayt)
const iv = CryptoJS.lib.WordArray.create(encryptedData.words.slice(0, 4));
// 3. Ciphertext (qolgan qism)
const ciphertext = CryptoJS.lib.WordArray.create(
encryptedData.words.slice(4),
encryptedData.sigBytes - 16
);
// 4. Maxfiy kalit
const key = CryptoJS.enc.Utf8.parse("12345678901234567890123456789012");
// 5. CipherParams yaratish
const cipherParams = CryptoJS.lib.CipherParams.create({
ciphertext,
key,
iv
});
// 6. Dekodlash
const decrypted = CryptoJS.AES.decrypt(cipherParams, key, {
iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return decrypted.toString(CryptoJS.enc.Utf8);
} catch (error) {
return null;
}
};

55
constants/formatText.ts Normal file
View File

@@ -0,0 +1,55 @@
const latinToCyrillicMap = [
["O'", "Ў"], ["o'", "ў"],
["G'", "Ғ"], ["g'", "ғ"],
["Sh", "Ш"], ["sh", "ш"],
["Ch", "Ч"], ["ch", "ч"],
["A", "А"], ["B", "Б"], ["D", "Д"], ["E", "Е"], ["F", "Ф"],
["G", "Г"], ["H", "Ҳ"], ["I", "И"], ["J", "Ж"], ["K", "К"],
["L", "Л"], ["M", "М"], ["N", "Н"], ["O", "О"], ["P", "П"],
["Q", "Қ"], ["R", "Р"], ["S", "С"], ["T", "Т"], ["U", "У"],
["V", "В"], ["X", "Х"], ["Y", "Й"], ["Z", "З"],
["a", "а"], ["b", "б"], ["d", "д"], ["e", "е"], ["f", "ф"],
["g", "г"], ["h", "ҳ"], ["i", "и"], ["j", "ж"], ["k", "к"],
["l", "л"], ["m", "м"], ["n", "н"], ["o", "о"], ["p", "п"],
["q", "қ"], ["r", "р"], ["s", "с"], ["t", "т"], ["u", "у"],
["v", "в"], ["x", "х"], ["y", "й"], ["z", "з"],
["'", ""] // apostrofni olib tashlaymiz
];
export function formatText(str: string | null) {
if (!str) return null;
let result = str;
for (let [latin, cyrillic] of latinToCyrillicMap) {
const regex = new RegExp(latin, "g");
result = result.replace(regex, cyrillic);
}
return result;
}
const cyrillicToLatinMap = [
["Ў", "O'"], ["ў", "o'"],
["Ғ", "G'"], ["ғ", "g'"],
["Ш", "Sh"], ["ш", "sh"],
["Ч", "Ch"], ["ч", "ch"],
["ё", "yo"], ["Ё", "YO"],
["А", "A"], ["Б", "B"], ["Д", "D"], ["Е", "E"], ["Ф", "F"],
["Г", "G"], ["Ҳ", "H"], ["И", "I"], ["Ж", "J"], ["К", "K"],
["Л", "L"], ["М", "M"], ["Н", "N"], ["О", "O"], ["П", "P"],
["Қ", "Q"], ["Р", "R"], ["С", "S"], ["Т", "T"], ["У", "U"],
["В", "V"], ["Х", "X"], ["Й", "Y"], ["З", "Z"],
["а", "a"], ["б", "b"], ["д", "d"], ["е", "e"], ["ф", "f"],
["г", "g"], ["ҳ", "h"], ["и", "i"], ["ж", "j"], ["к", "k"],
["л", "l"], ["м", "m"], ["н", "n"], ["о", "o"], ["п", "p"],
["қ", "q"], ["р", "r"], ["с", "s"], ["т", "t"], ["у", "u"],
["в", "v"], ["х", "x"], ["й", "y"], ["з", "z"],
];
export function formatTextToLatin(str: string | null) {
if (!str) return null;
let result = str;
for (let [cyrillic, latin] of cyrillicToLatinMap) {
const regex = new RegExp(cyrillic, "g");
result = result.replace(regex, latin);
}
return result;
}

View File

@@ -19,10 +19,7 @@ export const deleteAsyncStorage = async (name: string) => {
export const saveLang = async (lang: string) => { export const saveLang = async (lang: string) => {
try { try {
await AsyncStorage.setItem('lang', lang); await AsyncStorage.setItem('lang', lang);
console.log(`Language saved: ${lang}`); } catch (error) { }
} catch (error) {
console.error('Failed to save language', error);
}
}; };
/** /**
@@ -34,7 +31,6 @@ export const getLang = async (): Promise<string | null> => {
const lang = await AsyncStorage.getItem('lang'); const lang = await AsyncStorage.getItem('lang');
return lang; return lang;
} catch (error) { } catch (error) {
console.error('Failed to get language', error);
return null; return null;
} }
}; };
@@ -45,9 +41,7 @@ export const getLang = async (): Promise<string | null> => {
export const deleteLang = async () => { export const deleteLang = async () => {
try { try {
await AsyncStorage.removeItem('lang'); await AsyncStorage.removeItem('lang');
} catch (error) { } catch (error) { }
console.error('Failed to delete language', error);
}
}; };
const ACCESS = 'access_token'; const ACCESS = 'access_token';

View File

@@ -45,7 +45,6 @@ export function useNotifications() {
// Foreground listener // Foreground listener
notificationListener.current = Notifications.addNotificationReceivedListener((notification) => { notificationListener.current = Notifications.addNotificationReceivedListener((notification) => {
console.log('Notification received:', notification);
}); });
// User response listener // User response listener

View File

@@ -139,7 +139,7 @@
"Keyingi": "Next", "Keyingi": "Next",
"Xizmat sarlavhasi": "Service title", "Xizmat sarlavhasi": "Service title",
"Yangilashda xato yuz berdi": "Error occurred while updating", "Yangilashda xato yuz berdi": "Error occurred while updating",
"Xizmatni tahrirlash (1/2)": "Edit service (1/2)", "Xizmatni tahrirlash": "Edit service",
"Xizmatni tahrirlash (2/2)": "Edit service (2/2)", "Xizmatni tahrirlash (2/2)": "Edit service (2/2)",
"Tilni tanlang": "Select language", "Tilni tanlang": "Select language",
"Rejimni tanlang": "Select mode", "Rejimni tanlang": "Select mode",
@@ -226,5 +226,8 @@
"INN kiriting (9 raqam)": "Enter INN (9 digits)", "INN kiriting (9 raqam)": "Enter INN (9 digits)",
"Referal kodi": "Referral code", "Referal kodi": "Referral code",
"Direktor JSHSHR": "Director JSHSHR", "Direktor JSHSHR": "Director JSHSHR",
"Direktor JSHSHR (14 raqam)": "Director JSHSHR (number 14)" "Direktor JSHSHR (14 raqam)": "Director JSHSHR (number 14)",
"Hozircha bildirishnomalar yo'q": "No notifications yet",
"Siz o'zini o'zi band qilgan yoki yakka tartibdagi tadbirkorlik bo'lishingiz kerak": "You must be self-employed or an individual entrepreneur",
"Sizning shaxsiy ma'lumotlaringiz topilmadi": "Your personal information was not found"
} }

View File

@@ -139,7 +139,7 @@
"Keyingi": "Далее", "Keyingi": "Далее",
"Xizmat sarlavhasi": "Заголовок услуги", "Xizmat sarlavhasi": "Заголовок услуги",
"Yangilashda xato yuz berdi": "Произошла ошибка при обновлении", "Yangilashda xato yuz berdi": "Произошла ошибка при обновлении",
"Xizmatni tahrirlash (1/2)": "Редактирование услуги (1/2)", "Xizmatni tahrirlash": "Редактирование услуги",
"Xizmatni tahrirlash (2/2)": "Редактирование услуги (2/2)", "Xizmatni tahrirlash (2/2)": "Редактирование услуги (2/2)",
"Tilni tanlang": "Выберите язык", "Tilni tanlang": "Выберите язык",
"Rejimni tanlang": "Выберите режим", "Rejimni tanlang": "Выберите режим",
@@ -225,5 +225,8 @@
"INN kiriting (9 raqam)": "Введите ИНН (9 цифр)", "INN kiriting (9 raqam)": "Введите ИНН (9 цифр)",
"Referal kodi": "Реферальный код", "Referal kodi": "Реферальный код",
"Direktor JSHSHR": "Директор ПИНФЛ", "Direktor JSHSHR": "Директор ПИНФЛ",
"Direktor JSHSHR kiriting (14 raqam)": "Введите ПИНФЛ директора (14 цифр)" "Direktor JSHSHR kiriting (14 raqam)": "Введите ПИНФЛ директора (14 цифр)",
"Hozircha bildirishnomalar yo'q": "Пока нет уведомлений",
"Siz o'zini o'zi band qilgan yoki yakka tartibdagi tadbirkorlik bo'lishingiz kerak": "Вы должны быть самозанятым или индивидуальным предпринимателем",
"Sizning shaxsiy ma'lumotlaringiz topilmadi": "Ваши личные данные не найдены"
} }

View File

@@ -139,7 +139,7 @@
"Yangi xizmat (2/2)": "Yangi xizmat (2/2)", "Yangi xizmat (2/2)": "Yangi xizmat (2/2)",
"Keyingi": "Keyingi", "Keyingi": "Keyingi",
"Xizmat sarlavhasi": "Xizmat sarlavhasi", "Xizmat sarlavhasi": "Xizmat sarlavhasi",
"Xizmatni tahrirlash (1/2)": "Xizmatni tahrirlash (1/2)", "Xizmatni tahrirlash": "Xizmatni tahrirlash",
"Xizmatni tahrirlash (2/2)": "Xizmatni tahrirlash (2/2)", "Xizmatni tahrirlash (2/2)": "Xizmatni tahrirlash (2/2)",
"Tilni tanlang": "Tilni tanlang", "Tilni tanlang": "Tilni tanlang",
"Rejimni tanlang": "Rejimni tanlang", "Rejimni tanlang": "Rejimni tanlang",
@@ -177,6 +177,7 @@
"Faqat tasdiqlangan profillarga ishonch bilan murojaat qiling.": "Faqat tasdiqlangan profillarga ishonch bilan murojaat qiling.", "Faqat tasdiqlangan profillarga ishonch bilan murojaat qiling.": "Faqat tasdiqlangan profillarga ishonch bilan murojaat qiling.",
"Qo'llanma video": "Qo'llanma video", "Qo'llanma video": "Qo'llanma video",
"Bildirishnomalar": "Bildirishnomalar", "Bildirishnomalar": "Bildirishnomalar",
"Hozircha bildirishnomalar yo'q": "Hozircha bildirishnomalar yo'q",
"Bildirishnomalarni yuklashda muammo bo'ldi": "Bildirishnomalarni yuklashda muammo bo'ldi", "Bildirishnomalarni yuklashda muammo bo'ldi": "Bildirishnomalarni yuklashda muammo bo'ldi",
"Hozir": "Hozir", "Hozir": "Hozir",
"daqiqa oldin": "daqiqa oldin", "daqiqa oldin": "daqiqa oldin",
@@ -225,5 +226,7 @@
"INN kiriting (9 raqam)": "INN kiriting (9 raqam)", "INN kiriting (9 raqam)": "INN kiriting (9 raqam)",
"Referal kodi": "Referal kodi", "Referal kodi": "Referal kodi",
"Direktor JSHSHR": "Direktor JSHSHR", "Direktor JSHSHR": "Direktor JSHSHR",
"Direktor JSHSHR kiriting (14 raqam)": "Direktor JSHSHR kiriting (14 raqam)" "Direktor JSHSHR kiriting (14 raqam)": "Direktor JSHSHR kiriting (14 raqam)",
"Siz o'zini o'zi band qilgan yoki yakka tartibdagi tadbirkorlik bo'lishingiz kerak": "Siz o'zini o'zi band qilgan yoki yakka tartibdagi tadbirkorlik bo'lishingiz kerak",
"Sizning shaxsiy ma'lumotlaringiz topilmadi": "Sizning shaxsiy ma'lumotlaringiz topilmadi"
} }

96
package-lock.json generated
View File

@@ -23,6 +23,7 @@
"@react-navigation/native": "^7.1.8", "@react-navigation/native": "^7.1.8",
"@tanstack/react-query": "^5.90.17", "@tanstack/react-query": "^5.90.17",
"axios": "^1.13.2", "axios": "^1.13.2",
"crypto-js": "^4.2.0",
"expo": "~54.0.31", "expo": "~54.0.31",
"expo-av": "~16.0.8", "expo-av": "~16.0.8",
"expo-blur": "~15.0.8", "expo-blur": "~15.0.8",
@@ -71,9 +72,11 @@
"react-native-worklets": "^0.5.1", "react-native-worklets": "^0.5.1",
"react-stately": "^3.39.0", "react-stately": "^3.39.0",
"tailwind-variants": "^0.1.20", "tailwind-variants": "^0.1.20",
"toastify-react-native": "^7.2.3",
"zustand": "^5.0.10" "zustand": "^5.0.10"
}, },
"devDependencies": { "devDependencies": {
"@types/crypto-js": "^4.2.2",
"@types/react": "~19.1.0", "@types/react": "~19.1.0",
"babel-plugin-module-resolver": "^5.0.0", "babel-plugin-module-resolver": "^5.0.0",
"eslint": "^9.25.0", "eslint": "^9.25.0",
@@ -6536,6 +6539,13 @@
"@babel/types": "^7.28.2" "@babel/types": "^7.28.2"
} }
}, },
"node_modules/@types/crypto-js": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz",
"integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/estree": { "node_modules/@types/estree": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -8568,6 +8578,12 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
"license": "MIT"
},
"node_modules/crypto-random-string": { "node_modules/crypto-random-string": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
@@ -15396,6 +15412,73 @@
"react-native-pager-view": ">= 6.0.0" "react-native-pager-view": ">= 6.0.0"
} }
}, },
"node_modules/react-native-vector-icons": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-10.3.0.tgz",
"integrity": "sha512-IFQ0RE57819hOUdFvgK4FowM5aMXg7C7XKsuGLevqXkkIJatc3QopN0wYrb2IrzUgmdpfP+QVIbI3S6h7M0btw==",
"deprecated": "react-native-vector-icons package has moved to a new model of per-icon-family packages. See the https://github.com/oblador/react-native-vector-icons/blob/master/MIGRATION.md on how to migrate",
"license": "MIT",
"dependencies": {
"prop-types": "^15.7.2",
"yargs": "^16.1.1"
},
"bin": {
"fa-upgrade.sh": "bin/fa-upgrade.sh",
"fa5-upgrade": "bin/fa5-upgrade.sh",
"fa6-upgrade": "bin/fa6-upgrade.sh",
"generate-icon": "bin/generate-icon.js"
}
},
"node_modules/react-native-vector-icons/node_modules/cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^7.0.0"
}
},
"node_modules/react-native-vector-icons/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/react-native-vector-icons/node_modules/yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
"license": "MIT",
"dependencies": {
"cliui": "^7.0.2",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.0",
"y18n": "^5.0.5",
"yargs-parser": "^20.2.2"
},
"engines": {
"node": ">=10"
}
},
"node_modules/react-native-vector-icons/node_modules/yargs-parser": {
"version": "20.2.9",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/react-native-web": { "node_modules/react-native-web": {
"version": "0.21.2", "version": "0.21.2",
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz", "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz",
@@ -17202,6 +17285,19 @@
"node": ">=8.0" "node": ">=8.0"
} }
}, },
"node_modules/toastify-react-native": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/toastify-react-native/-/toastify-react-native-7.2.3.tgz",
"integrity": "sha512-ngmpTKlTo0IRddwSsNWK+YKbB2veqotHy7Zpil4eksoLAlq0RPSgdVOk5QDEDUONJQ4r7ljGYeRW68KBztirsg==",
"license": "MIT",
"dependencies": {
"react-native-vector-icons": "*"
},
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/toidentifier": { "node_modules/toidentifier": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",

View File

@@ -28,6 +28,7 @@
"@react-navigation/native": "^7.1.8", "@react-navigation/native": "^7.1.8",
"@tanstack/react-query": "^5.90.17", "@tanstack/react-query": "^5.90.17",
"axios": "^1.13.2", "axios": "^1.13.2",
"crypto-js": "^4.2.0",
"expo": "~54.0.31", "expo": "~54.0.31",
"expo-av": "~16.0.8", "expo-av": "~16.0.8",
"expo-blur": "~15.0.8", "expo-blur": "~15.0.8",
@@ -76,9 +77,11 @@
"react-native-worklets": "^0.5.1", "react-native-worklets": "^0.5.1",
"react-stately": "^3.39.0", "react-stately": "^3.39.0",
"tailwind-variants": "^0.1.20", "tailwind-variants": "^0.1.20",
"toastify-react-native": "^7.2.3",
"zustand": "^5.0.10" "zustand": "^5.0.10"
}, },
"devDependencies": { "devDependencies": {
"@types/crypto-js": "^4.2.2",
"@types/react": "~19.1.0", "@types/react": "~19.1.0",
"babel-plugin-module-resolver": "^5.0.0", "babel-plugin-module-resolver": "^5.0.0",
"eslint": "^9.25.0", "eslint": "^9.25.0",

View File

@@ -101,14 +101,14 @@ export default function DashboardScreen() {
// Announcement videos // Announcement videos
const videos = { const videos = {
uz: require('@/assets/announcements-video/video_uz.webm'), uz: require('@/assets/announcements-video/video_uz.mp4'),
ru: require('@/assets/announcements-video/video_ru.webm'), ru: require('@/assets/announcements-video/video_ru.mp4'),
en: require('@/assets/announcements-video/video_en.webm'), en: require('@/assets/announcements-video/video_en.mp4'),
}; };
// Government videos: faqat RU mavjud // Government videos: faqat RU mavjud
const govermentVideos: Partial<Record<'uz' | 'ru' | 'en', any>> = { const govermentVideos: Partial<Record<'uz' | 'ru' | 'en', any>> = {
ru: require('@/assets/goverment/video_ru.webm'), ru: require('@/assets/goverment/video_ru.mp4'),
}; };
// Update selected language // Update selected language

View File

@@ -16,12 +16,11 @@ import {
Platform, Platform,
StyleSheet, StyleSheet,
Text, Text,
ToastAndroid,
TouchableOpacity, TouchableOpacity,
View, View
} from 'react-native'; } from 'react-native';
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
import { SafeAreaView } from 'react-native-safe-area-context'; import { Toast } from 'toastify-react-native';
import { auth_api } from '../login/lib/api'; import { auth_api } from '../login/lib/api';
import useTokenStore from '../login/lib/hook'; import useTokenStore from '../login/lib/hook';
import ConfirmForm from './ConfirmForm'; import ConfirmForm from './ConfirmForm';
@@ -44,9 +43,7 @@ const ConfirmScreen = () => {
const storedPhone = await AsyncStorage.getItem('phone'); const storedPhone = await AsyncStorage.getItem('phone');
if (storedPhone) setPhone(storedPhone); if (storedPhone) setPhone(storedPhone);
else setPhone(null); else setPhone(null);
} catch (error) { } catch (error) { }
console.log('AsyncStorage error:', error);
}
}; };
loadPhone(); loadPhone();
}, []); }, []);
@@ -98,11 +95,11 @@ const ConfirmScreen = () => {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
setResendTimer(60); setResendTimer(60);
if (Platform.OS === 'android') { if (Platform.OS === 'android') {
ToastAndroid.show(t('Kod qayta yuborildi'), ToastAndroid.SHORT); Toast.info(t('Kod qayta yuborildi'));
} }
}, },
onError: () => { onError: () => {
Alert.alert(t('Xatolik yuz berdi'), t('Kodni qayta yuborishda xatolik yuz berdi')); Toast.error(t('Kodni qayta yuborishda xatolik yuz berdi'));
}, },
}); });
@@ -130,10 +127,10 @@ const ConfirmScreen = () => {
<View style={styles.decorCircle1} /> <View style={styles.decorCircle1} />
<View style={styles.decorCircle2} /> <View style={styles.decorCircle2} />
<AuthHeader /> <AuthHeader />
<SafeAreaView style={{ flex: 1 }}>
<KeyboardAwareScrollView <KeyboardAwareScrollView
contentContainerStyle={styles.scrollContent} contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false} enableOnAndroid
extraScrollHeight={120}
keyboardShouldPersistTaps="handled" keyboardShouldPersistTaps="handled"
> >
<View style={styles.header}> <View style={styles.header}>
@@ -195,7 +192,6 @@ const ConfirmScreen = () => {
</Text> </Text>
</View> */} </View> */}
</KeyboardAwareScrollView> </KeyboardAwareScrollView>
</SafeAreaView>
</View> </View>
); );
}; };

View File

@@ -1,6 +1,6 @@
import httpClient from '@/api/httpClient'; import httpClient from '@/api/httpClient';
import { API_URLS } from '@/api/URLs'; import { API_URLS } from '@/api/URLs';
import { AxiosResponse } from 'axios'; import axios, { AxiosResponse } from 'axios';
interface ConfirmBody { interface ConfirmBody {
status: boolean; status: boolean;
@@ -13,307 +13,70 @@ interface ConfirmBody {
}; };
} }
export interface CompanyInfo { export interface GetInfo {
id: number; VATRegCode: null | string
inn: string; VATRegStatus: null | string
registration_authority: string; account: string
registration_date: string; accountant: null | string
registration_number: string; address: string
name: string; bankAccount: string
short_name: string; bankCode: string
opf_code: string; director: string | null
opf_name: string; directorPinfl: string | null
oked_code: string; directorTin: null | string
vat_number: string; fullName: string
oked_name: string; fullname: string
area: string; isBudget: number
region: string; isItd: boolean
soogu_code: string; mfo: string
soogu_name: string; na1Code: null | string
small_businesses: string; na1Name: null | string
activity_state: number; name: string
statutory_fund: string; ns10Code: number
ns11Code: number
activity_state_detail: ActivityStateDetail; oked: null | string
business_type_detail: BusinessTypeDetail; peasantFarm: boolean
personalNum: string
director: string; privateNotary: boolean
email: string; regDate: null | string
village_code: string; selfEmployment: boolean
email_status: number; shortName: string
phones: string[]; shortname: string
statusCode: null | string
soato_code: string; statusName: null | string
soato_name: string; tin: string
address: string;
relevance_date: string;
court_relevance_date: string | null;
deal_relevance_date: string | null;
tax_mode: number;
trust: string;
score: number;
itpark: number;
leasing_count_all: number | null;
leasing_count_not_finish: number | null;
leasing_sum: number | null;
leasing_is_delay: number | null;
leasing_debt: number | null;
leasing_is_partner: number | null;
leasing_relevance_date: string | null;
is_bankrupt: number;
is_abuse_vat: number;
is_large_taxpayer: number;
vendor_rating: number | null;
developer_rating: number | null;
dishonest_executor: DishonestExecutor;
village_detail: VillageDetail;
company_billing_address: BillingAddress;
actual_date: string;
kfs: Kfs;
uuid: string;
connections: Connections;
courts: Courts;
director_uuid: string;
founders: Founder[];
deals: Deals;
licenses: Licenses;
leasing_guarantor_pinfl: string | null;
leasing_guarantor_inn: string | null;
buildings: Buildings;
cadastres: Cadastres;
liquidation_date: string | null;
liquidation_reason: string | null;
is_suppliers: number;
}
export interface ActivityStateDetail {
id: number;
group: string;
name: string;
name_en: string;
name_uz: string;
name_uz_kir: string;
} }
export interface BusinessTypeDetail { export interface GetDistrict {
id: number; districtId: string,
external_id: number; regionId: number,
name: string; districtCode: number,
name_uz: string; name: string
name_en: string;
} }
export interface DishonestExecutor { export interface GetRegion {
is_dishonest_executor: number; regionId: number,
delete_date: string | null; name: string
} }
export interface VillageDetail { export interface GetTokens {
code: number;
name: string;
}
export interface BillingAddress {
country_code: number;
region_code: number;
region_name: string;
region_name_en: string;
region_name_uz: string;
district_code: number;
district_name: string;
district_name_en: string;
district_name_uz: string;
sector_code: number;
street_name: string;
house: string | null;
flat: string | null;
postcode: string;
}
export interface Kfs {
code: number;
name: string;
name_ru: string;
name_uz_cyr: string;
name_uz_lat: string;
}
export interface Connections {
director: number;
founders: number;
entrepreneur: number;
all: number;
}
export interface Courts {
total: number;
current: number;
completed: number;
}
export interface Founder {
name: string;
percentage: number;
is_individual: number;
person_type: string;
id: number | null;
founder_uuid: string;
}
export interface Deals {
customer: DealSide;
provider: DealSide;
actual_date: string;
}
export interface DealSide {
rows: any[];
total: number;
summ: number | null;
}
export interface Licenses {
total: number;
relevance_date: string;
actual_date: string;
}
export interface Buildings {
total: number;
}
export interface Cadastres {
total: number;
relevance_date: string | null;
}
export interface GetInfoResponse {
status: boolean;
data: CompanyInfo | IndividualInfo;
}
export interface IndividualInfo {
uuid: string,
id: number,
lastname: string,
firstname: string,
middlename: string,
registered_at: string,
unregistered_at: null | string,
activities: {
code: number,
name_en: string,
name_uz: string,
name_ru: string
}[]
}
export interface GetDirectorInfoResponse {
status: boolean, status: boolean,
data: { data: {
entity: { links: {
name: { previous: null | string,
rows: { next: null | string
id: number,
inn: string,
name: string,
director: string,
email: string,
phones: string[],
founders: {
name: string,
percentage: number,
is_individual: number,
person_type: string,
id: number | null,
founder_uuid: string
}[],
activity_state: number,
registration_date: string,
oked_code: string,
oked_name: string,
statutory_fund: string,
address: string,
variant: null
}[],
total: number
}, },
inn: { total_items: number,
rows: [], total_pages: number,
total: 0 page_size: number,
}, current_page: number,
director: { results:
rows: [],
total: 0
},
founder: {
rows: {
id: number,
inn: string,
name: string,
director: string,
email: string,
phones: string[],
founders:
{
name: string,
percentage: number,
is_individual: number,
person_type: string,
id: number | null,
founder_uuid: string
}[],
activity_state: number,
registration_date: string,
oked_code: string,
oked_name: string,
statutory_fund: string,
address: string,
variant: null
}[],
total: number
},
email: {
rows: [],
total: number
},
phone: {
rows: [],
total: number
}
},
entrepreneur: {
rows:
{ {
id: number, id: number,
pinfl: string, key: string,
entrepreneur: string, value: string,
email: string, is_active: boolean
phone: string, }[]
registration_date: string
}[],
total: number
},
trademark: {
rows: [],
total: number
}
} }
} }
@@ -332,25 +95,60 @@ export const auth_api = {
return res; return res;
}, },
async get_info(body: { value: string, type: string, passport_series?: string, passport_number?: string }): Promise<AxiosResponse<GetInfoResponse>> { async get_info(body: { value: string, token: string, tokenName: string }): Promise<AxiosResponse<GetInfo>> {
const res = await httpClient.post(API_URLS.Info, body); try {
const res = await axios.get(`https://testapi3.didox.uz/v1/utils/info/${body.value}`, {
headers: {
"Accept-Language": "uz",
[body.tokenName]: body.token
}
});
return res; return res;
} catch (error) {
throw error;
}
}, },
async get_director_info(body: { value: string }): Promise<AxiosResponse<GetDirectorInfoResponse>> { async get_district(body: { token: string, tokenName: string }): Promise<AxiosResponse<GetDistrict[]>> {
const res = await httpClient.post(API_URLS.Get_Director_Info, body); try {
const res = await axios.get(`https://testapi3.didox.uz/v1/districts/all/`, {
headers: {
"Accept-Language": "uz",
[body.tokenName]: body.token
}
});
return res; return res;
} catch (error) {
throw error;
}
},
async get_region(body: { token: string, tokenName: string }): Promise<AxiosResponse<GetRegion[]>> {
try {
const res = await axios.get(`https://testapi3.didox.uz/v1/regions/all/`, {
headers: {
"Accept-Language": "uz",
[body.tokenName]: body.token
}
});
return res;
} catch (error) {
throw error;
}
}, },
async register(body: { async register(body: {
phone: string; phone: string;
stir: string; stir: string;
person_type: string; person_type: string;
referal: string; referral: string;
activate_types: number[]; activate_types: number[];
director_full_name: string; director_full_name: string;
first_name: string; first_name: string;
last_name: string; last_name: string;
district: number;
address: number;
company_name: string;
}) { }) {
const res = await httpClient.post(API_URLS.Register, body); const res = await httpClient.post(API_URLS.Register, body);
return res; return res;
@@ -365,4 +163,10 @@ export const auth_api = {
const res = await httpClient.post(API_URLS.Register_Resend, body); const res = await httpClient.post(API_URLS.Register_Resend, body);
return res; return res;
}, },
async get_tokens(): Promise<AxiosResponse<GetTokens>> {
const res = httpClient.get(API_URLS.GetTokens)
return res
},
}; };

View File

@@ -19,7 +19,6 @@ export default function LoginForm() {
const [focused, setFocused] = useState(false); const [focused, setFocused] = useState(false);
const scaleAnim = useRef(new Animated.Value(1)).current; const scaleAnim = useRef(new Animated.Value(1)).current;
const { phone, setPhone, submit, loading, error } = UseLoginForm(); const { phone, setPhone, submit, loading, error } = UseLoginForm();
console.log(error);
const { t } = useTranslation(); const { t } = useTranslation();
const handleChange = useCallback( const handleChange = useCallback(

View File

@@ -16,12 +16,12 @@ import {
Platform, Platform,
StyleSheet, StyleSheet,
Text, Text,
ToastAndroid,
TouchableOpacity, TouchableOpacity,
View, View
} from 'react-native'; } from 'react-native';
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { Toast } from 'toastify-react-native';
import { auth_api } from '../login/lib/api'; import { auth_api } from '../login/lib/api';
import useTokenStore from '../login/lib/hook'; import useTokenStore from '../login/lib/hook';
import ConfirmForm from './ConfirmForm'; import ConfirmForm from './ConfirmForm';
@@ -44,9 +44,7 @@ const RegisterConfirmScreen = () => {
const storedPhone = await AsyncStorage.getItem('phone'); const storedPhone = await AsyncStorage.getItem('phone');
if (storedPhone) setPhone(storedPhone); if (storedPhone) setPhone(storedPhone);
else setPhone(null); else setPhone(null);
} catch (error) { } catch (error) { }
console.log('AsyncStorage error:', error);
}
}; };
loadPhone(); loadPhone();
}, []); }, []);
@@ -67,6 +65,7 @@ const RegisterConfirmScreen = () => {
savedToken(res.data.data.token.access); savedToken(res.data.data.token.access);
await AsyncStorage.setItem('refresh_token', res.data.data.token.refresh); await AsyncStorage.setItem('refresh_token', res.data.data.token.refresh);
await login(res.data.data.token.access); await login(res.data.data.token.access);
const pushToken = await registerForPushNotificationsAsync(); const pushToken = await registerForPushNotificationsAsync();
if (pushToken) { if (pushToken) {
await commonRequests.registerDevice({ await commonRequests.registerDevice({
@@ -78,6 +77,8 @@ const RegisterConfirmScreen = () => {
// Notification querylarni refetch // Notification querylarni refetch
queryClient.refetchQueries({ queryKey: ['notification-list'] }); queryClient.refetchQueries({ queryKey: ['notification-list'] });
queryClient.refetchQueries({ queryKey: ['notifications-list'] }); queryClient.refetchQueries({ queryKey: ['notifications-list'] });
// Dashboardga yonaltirish
router.replace('/(dashboard)'); router.replace('/(dashboard)');
}, },
onError: (err: any) => { onError: (err: any) => {
@@ -93,7 +94,7 @@ const RegisterConfirmScreen = () => {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
setResendTimer(60); setResendTimer(60);
if (Platform.OS === 'android') { if (Platform.OS === 'android') {
ToastAndroid.show(t('Kod qayta yuborildi'), ToastAndroid.SHORT); Toast.info(t('Kod qayta yuborildi'));
} }
}, },
onError: () => { onError: () => {

View File

@@ -9,7 +9,7 @@ import { ScrollView, StyleSheet, Text, TouchableOpacity } from 'react-native';
export default function CategorySelectScreen() { export default function CategorySelectScreen() {
const router = useRouter(); const router = useRouter();
const { phone, stir, person_type, director_full_name, referal, first_name, last_name } = useLocalSearchParams<{ const { phone, stir, person_type, director_full_name, referal, first_name, last_name, address, company_name } = useLocalSearchParams<{
phone: string; phone: string;
stir: string; stir: string;
person_type: 'band' | 'ytt'; person_type: 'band' | 'ytt';
@@ -17,10 +17,10 @@ export default function CategorySelectScreen() {
director_full_name: string; director_full_name: string;
first_name: string; first_name: string;
last_name: string; last_name: string;
address: string;
company_name: string;
}>(); }>();
console.log(referal);
const [selected, setSelected] = React.useState<number | null>(null); const [selected, setSelected] = React.useState<number | null>(null);
const { data } = useQuery({ const { data } = useQuery({
@@ -34,10 +34,13 @@ export default function CategorySelectScreen() {
stir: string; stir: string;
person_type: string; person_type: string;
activate_types: number[]; activate_types: number[];
referal: string; referral: string;
director_full_name: string; director_full_name: string;
first_name: string; first_name: string;
last_name: string; last_name: string;
district: number;
company_name: string;
address: number;
}) => auth_api.register(body), }) => auth_api.register(body),
onSuccess: () => router.replace('/'), onSuccess: () => router.replace('/'),
}); });
@@ -68,10 +71,13 @@ export default function CategorySelectScreen() {
person_type: person_type, person_type: person_type,
phone: `998${phone}`, phone: `998${phone}`,
stir: stir, stir: stir,
referal: String(referal), referral: String(referal),
director_full_name: String(director_full_name), director_full_name: String(director_full_name),
first_name: String(first_name), first_name: String(first_name),
last_name: String(last_name), last_name: String(last_name),
district: Number(address),
company_name: String(company_name),
address: Number(address),
}); });
} }
}} }}

View File

@@ -1,106 +1,108 @@
import AuthHeader from '@/components/ui/AuthHeader'; import AuthHeader from '@/components/ui/AuthHeader';
import { decryptToken } from '@/constants/crypto';
import { formatPhone, normalizeDigits } from '@/constants/formatPhone'; import { formatPhone, normalizeDigits } from '@/constants/formatPhone';
import { formatText, formatTextToLatin } from '@/constants/formatText';
import { products_api } from '@/screens/home/lib/api'; import { products_api } from '@/screens/home/lib/api';
import BottomSheet, { import BottomSheet, { BottomSheetBackdrop, BottomSheetFlatList, BottomSheetTextInput } from '@gorhom/bottom-sheet';
BottomSheetBackdrop,
BottomSheetFlatList,
BottomSheetTextInput,
} from '@gorhom/bottom-sheet';
import { useMutation, useQuery } from '@tanstack/react-query'; import { useMutation, useQuery } from '@tanstack/react-query';
import { AxiosError } from 'axios';
import { Image } from 'expo-image'; import { Image } from 'expo-image';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import { import {
Building2,
CheckIcon, CheckIcon,
ChevronDown,
Globe, Globe,
Hash, Hash,
Search, Search,
ShieldCheck, UserPlus
User,
UserPlus,
} from 'lucide-react-native'; } from 'lucide-react-native';
import React, { useCallback, useMemo, useRef, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
ActivityIndicator, ActivityIndicator,
KeyboardAvoidingView, Keyboard,
Platform,
ScrollView,
StyleSheet, StyleSheet,
Text, Text,
TextInput, TextInput,
TouchableOpacity, TouchableOpacity,
View, View
} from 'react-native'; } from 'react-native';
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { auth_api } from '../login/lib/api'; import { auth_api, GetInfo } from '../login/lib/api';
import PhonePrefix from '../login/ui/PhonePrefix'; import PhonePrefix from '../login/ui/PhonePrefix';
import { useRegister } from './lib/useRegisterStore'; import { UseLoginForm } from '../login/ui/UseLoginForm';
function getHeaderInfo(personType: string | null) { interface CoordsData {
switch (personType) { lat: number;
case 'yatt': lon: number;
return { polygon: [number, number][][];
label: 'YATT',
icon: <User size={32} color="#ffffff" />,
gradient: ['#10b981', '#059669'] as [string, string],
};
case 'band':
return {
label: "O'zini o'zi band qilgan",
icon: <ShieldCheck size={32} color="#ffffff" />,
gradient: ['#3b82f6', '#2563eb'] as [string, string],
};
case 'legal_entity':
return {
label: 'Yuridik shaxs',
icon: <Building2 size={32} color="#ffffff" />,
gradient: ['#f59e0b', '#d97706'] as [string, string],
};
default:
return {
label: "Ro'yxatdan o'tish",
icon: <UserPlus size={32} color="#ffffff" />,
gradient: ['#10b981', '#059669'] as [string, string],
};
}
} }
export default function RegisterFormScreen() { export default function RegisterFormScreen() {
const router = useRouter(); const router = useRouter();
const {
personType,
phone,
setPhone,
referal,
setReferal,
jshshr,
setJshshr,
passportSeries,
setPassportSeries,
passportNumber,
setPassportNumber,
inn,
setInn,
info,
setInfo,
directorInfo,
setDirectorInfo,
} = useRegister();
const { t } = useTranslation(); const { t } = useTranslation();
const { phone, setPhone } = UseLoginForm();
const [loading, setLoading] = React.useState(false);
const [directorJshshr, setDirectorJshshr] = React.useState('');
const [directorLoading, setDirectorLoading] = React.useState(false);
const [directorInfoError, setErrorDirectorInfo] = useState<string | null>(null);
// Country select
const [selectedCountry, setSelectedCountry] = useState<string>('UZ');
const [countrySearch, setCountrySearch] = useState<string>('');
const countrySheetRef = useRef<BottomSheet>(null); const countrySheetRef = useRef<BottomSheet>(null);
const snapPoints = useMemo(() => ['60%', '90%'], []); const snapPoints = useMemo(() => ['60%', '90%'], []);
const [selectedCountry, setSelectedCountry] = useState<string>('UZ');
const [countrySearch, setCountrySearch] = useState<string>('');
const [stir, setStir] = useState('');
const [info, setInfo] = useState<GetInfo | null>(null);
const [loading, setLoading] = useState(false);
const [referal, setReferal] = useState('');
const [error, setError] = useState<string | null>(null)
const [district, setDistrict] = useState<string | null>(null)
const [region, setRegion] = useState<string | null>(null)
const [token, setTokens] = useState<{ name: string, value: string } | null>(null)
const [directorTinInput, setDirectorTinInput] = useState('');
const { data } = useQuery({
queryKey: ["tokens"],
queryFn: async () => auth_api.get_tokens(),
select(data) {
return data.data.data.results
},
})
useEffect(() => {
if (data?.length) {
const token = data[0]
const tokenValue = decryptToken(token.value)
if (tokenValue) {
setTokens({ name: token.key, value: tokenValue })
}
}
}, [data])
const { mutate } = useMutation({
mutationFn: (stir: string) => auth_api.get_info({ value: stir, token: token?.value || "", tokenName: token?.name || "" }),
onSuccess: (res) => {
setInfo(res.data);
setLoading(false);
setError(null)
setDistrict(res.data.address)
},
onError: () => {
setInfo(null);
setLoading(false);
setError("Foydalanuvchi topilmadi")
},
});
const { data: districts } = useQuery({
queryKey: ["discrit"],
queryFn: async () => auth_api.get_district({ token: token?.value || "", tokenName: token?.name || "" }),
enabled: !!token,
})
const { data: regions } = useQuery({
queryKey: ["regions"],
queryFn: async () => auth_api.get_region({ token: token?.value || "", tokenName: token?.name || "" }),
enabled: !!token,
})
const { data: countryResponse, isLoading: countryLoading } = useQuery({ const { data: countryResponse, isLoading: countryLoading } = useQuery({
queryKey: ['country-detail'], queryKey: ['country-detail'],
@@ -108,16 +110,125 @@ export default function RegisterFormScreen() {
select: (res) => res.data?.data || [], select: (res) => res.data?.data || [],
}); });
const getRegionDistrictFromAddress = async (address: string) => {
try {
const encoded = encodeURIComponent(address + ", Uzbekistan");
const res = await fetch(
`https://nominatim.openstreetmap.org/search?q=${encoded}&format=json&addressdetails=1&limit=1`,
{ headers: { 'Accept-Language': 'uz', 'User-Agent': 'MyApp/1.0 (turgunboyevsamandar4@gamil.com)' } }
);
const data = await res.json();
if (data.length > 0) {
const addr = data[0].address;
return {
district: addr.county || addr.district || addr.suburb,
region: addr.state,
};
}
} catch (e) { }
return null;
};
useEffect(() => {
if (district) {
const dis = formatText(district)?.split(" ")[0].toLocaleUpperCase()
let reg = null
if (dis) {
reg = districts?.data.find((item) => item.name.includes(dis))
};
if (reg) {
const region = regions?.data.find((item) => item.regionId == reg.regionId)
setRegion(region?.name || "")
}
}
}, [district])
const [districtId, setDistrictId] = useState<number | null>(null);
const [regionId, setRegionId] = useState<number | null>(null);
useEffect(() => {
if (!district || !countryResponse?.length) return;
const resolve = async () => {
const geo = await getRegionDistrictFromAddress(district.split(" ")[0]);
const searchRegion = geo?.region || region;
const searchDistrict = geo?.district || district
for (const country of countryResponse) {
const regionName = formatTextToLatin(searchRegion)?.split(" ")[0];
let foundRegion = null
if (regionName) {
foundRegion = country.region.find((r: any) =>
formatTextToLatin(r.name)?.includes(regionName)
);
}
if (foundRegion) {
setRegionId(foundRegion.id);
setDistrictId(null);
return;
}
for (const reg of country.region || []) {
const dis = formatTextToLatin(searchDistrict)?.split(" ")[0].toUpperCase();
let foundDistrict = null
if (dis) {
foundDistrict = reg.districts.find((d: any) => {
return formatTextToLatin(d.name)?.toUpperCase().includes(dis.slice(0, 4))
}
);
};
if (foundDistrict) {
setDistrictId(foundDistrict.id);
setRegionId(null);
return;
}
}
}
};
resolve();
}, [district, countryResponse]);
useEffect(() => {
if (info === null || (stir.length === 9 && info.name && info.fullName)) {
setError(null)
} else if (info?.name === null || info?.fullName === null) {
setError("Sizning shaxsiy ma'lumotlaringiz topilmadi")
} else if (!info?.selfEmployment && !info?.isItd) {
setError("Siz o'zini o'zi band qilgan yoki yakka tartibdagi tadbirkorlik bo'lishingiz kerak")
}
}, [info])
const hasDirectorTin = info?.directorPinfl && String(info.directorPinfl).length > 0;
const isDirectorTinValid = !hasDirectorTin || directorTinInput === String(info.directorPinfl);
const hasValidName = Boolean(info?.name || info?.fullName);
const filteredCountries = useMemo(() => {
if (!countrySearch.trim()) return countryResponse || [];
const q = countrySearch.toLowerCase().trim();
return (countryResponse || []).filter((c: any) => c.name?.toLowerCase().includes(q));
}, [countryResponse, countrySearch]);
const openCountrySheet = useCallback(() => { const openCountrySheet = useCallback(() => {
Keyboard.dismiss();
setTimeout(() => { setTimeout(() => {
countrySheetRef.current?.snapToIndex(0); countrySheetRef.current?.snapToIndex(0);
}, 100); }, 100);
}, []); }, []);
const closeCountrySheet = useCallback(() => { const selectedCountryName = useMemo(() => {
countrySheetRef.current?.close(); if (!selectedCountry) return t('Tanlang');
setTimeout(() => setCountrySearch(''), 300); return (
}, []); countryResponse?.find((c: any) => c.flag?.toUpperCase() === selectedCountry)?.name ||
t('Tanlang')
);
}, [selectedCountry, countryResponse, t]);
const renderBackdrop = useCallback( const renderBackdrop = useCallback(
(props: any) => ( (props: any) => (
@@ -132,236 +243,34 @@ export default function RegisterFormScreen() {
[] []
); );
const selectedCountryName = useMemo(() => { const closeCountrySheet = useCallback(() => {
if (!selectedCountry) return t('Tanlang'); countrySheetRef.current?.close();
return ( setTimeout(() => setCountrySearch(''), 300);
countryResponse?.find((c: any) => c.flag?.toUpperCase() === selectedCountry)?.name || }, []);
t('Tanlang')
);
}, [selectedCountry, countryResponse, t]);
const filteredCountries = useMemo(() => { const valid =
if (!countrySearch.trim()) return countryResponse || []; phone.length === 9 &&
const q = countrySearch.toLowerCase().trim(); (stir.length === 9 || stir.length === 14) &&
return (countryResponse || []).filter((c: any) => c.name?.toLowerCase().includes(q));
}, [countryResponse, countrySearch]);
const headerInfo = getHeaderInfo(personType);
const isYattOrBand = personType === 'yatt' || personType === 'band';
const isLegal = personType === 'legal_entity';
const { mutate: fetchInfo } = useMutation({
mutationFn: (body: {
value: string;
type: string;
passport_series?: string;
passport_number?: string;
}) => auth_api.get_info(body),
onSuccess: (res) => {
setInfo(res.data.data);
setLoading(false);
// INN o'zgarganda director ma'lumotlarini tozalash
setDirectorJshshr('');
setDirectorInfo(null);
setErrorDirectorInfo(null);
},
onError: () => {
setInfo(null);
setLoading(false);
setDirectorJshshr('');
setDirectorInfo(null);
setErrorDirectorInfo(null);
},
});
const { mutate: fetchDirectorInfo } = useMutation({
mutationFn: (body: { value: string }) => auth_api.get_director_info(body),
onSuccess: (res) => {
const directorData = res.data;
// -------------------------------------------------------
// INN TEKSHIRUVI
// Director response strukturasi:
// data.entity.name.rows = [{ inn: "123456789", ... }, ...]
// Shu rows ichida joriy inn bor-yo'qligini tekshiramiz
// -------------------------------------------------------
const rows: Array<{ inn: string }> = directorData?.data?.entity?.name?.rows ?? [];
const innMatch = rows.some((row) => row.inn === inn);
if (!innMatch) {
// Director bu tashkilotga tegishli emas
setDirectorInfo(null);
setErrorDirectorInfo(t("Bu direktor ko'rsatilgan tashkilotga tegishli emas"));
setDirectorLoading(false);
return;
}
// INN mos keldi — director ma'lumotini saqlash
setDirectorInfo(directorData);
setDirectorLoading(false);
setErrorDirectorInfo(null);
},
onError: (error: AxiosError) => {
const err = error.response?.data as {
status: boolean;
data: {
detail: string;
error: { message: string };
status_code: number;
};
};
setDirectorInfo(null);
setErrorDirectorInfo(err?.data?.detail ?? t('Xatolik yuz berdi'));
setDirectorLoading(false);
},
});
const handleJshshrChange = useCallback(
(text: string) => {
const v = normalizeDigits(text).slice(0, 14);
setJshshr(v);
if (v.length === 14) {
setLoading(true);
if (personType) {
fetchInfo({ value: v, type: personType });
}
}
},
[setJshshr, fetchInfo, personType]
);
const handleInnChange = useCallback(
(text: string) => {
const v = normalizeDigits(text).slice(0, 9);
setInn(v);
if (v.length === 9) {
setLoading(true);
if (personType) {
fetchInfo({ value: v, type: personType });
}
}
},
[setInn, fetchInfo, personType]
);
const handlePassportSeriesChange = useCallback(
(text: string) => {
const v = text
.toUpperCase()
.replace(/[^A-Z]/g, '')
.slice(0, 2);
setPassportSeries(v);
if (personType && passportNumber.length === 7 && jshshr.length === 14) {
setLoading(true);
fetchInfo({
type: personType,
passport_number: passportNumber,
passport_series: v,
value: jshshr,
});
}
},
[setPassportSeries, passportNumber, jshshr, personType, fetchInfo]
);
const handlePassportNumberChange = useCallback(
(text: string) => {
const v = normalizeDigits(text).slice(0, 7);
setPassportNumber(v);
if (personType && passportSeries.length === 2 && jshshr.length === 14) {
setLoading(true);
fetchInfo({
type: personType,
passport_number: v,
passport_series: passportSeries,
value: jshshr,
});
}
},
[setPassportNumber, passportSeries, jshshr, personType, fetchInfo]
);
const handleDirectorJshshrChange = useCallback(
(text: string) => {
const v = normalizeDigits(text).slice(0, 14);
setDirectorJshshr(v);
setDirectorInfo(null);
setErrorDirectorInfo(null);
if (v.length === 14) {
setDirectorLoading(true);
fetchDirectorInfo({ value: v });
}
},
[fetchDirectorInfo]
);
const hasValidName = Boolean(info?.name || info?.fullName);
const hasValidInfo = Boolean(info?.lastname || info?.firstname || info?.middlename);
const isValid = (() => {
const phoneValid = phone.length === 9;
const referalValid = referal.length > 0;
const countryValid = Boolean(selectedCountry && selectedCountry !== 'all');
if (isYattOrBand) {
return (
phoneValid &&
referalValid &&
countryValid &&
jshshr.length === 14 &&
passportSeries.length === 2 &&
passportNumber.length === 7 &&
info
);
}
if (isLegal) {
return (
phoneValid &&
referalValid &&
countryValid &&
inn.length === 9 &&
info && info &&
directorJshshr.length === 14 && hasValidName &&
directorInfo // faqat INN mos kelganda to'ldiriladi isDirectorTinValid &&
); error === null;
}
return false;
})();
const handleContinue = () => {
// Director to'liq ismi directorInfo dan olinadi
const directorFullName = isLegal
? (directorInfo?.data?.entrepreneur?.rows[0]?.entrepreneur ?? '')
: `${info?.firstname ?? ''} ${info?.lastname ?? ''} ${info?.middlename ?? ''}`.trim();
const countryObj = countryResponse?.find((c: any) => c.flag?.toUpperCase() === selectedCountry);
router.push({
pathname: '/(auth)/select-category',
params: {
phone,
stir: isLegal ? inn : jshshr,
person_type: personType ?? '',
passport_series: passportSeries,
passport_number: passportNumber,
director_full_name: directorFullName,
referal: referal,
first_name: info?.firstname ?? '',
last_name: info?.lastname ?? '',
middle_name: info?.middlename ?? '',
country: countryObj?.name ?? '',
country_id: selectedCountry,
},
});
};
return ( return (
<View style={{ flex: 1 }}> <>
<View style={styles.container}> <KeyboardAwareScrollView
enableOnAndroid
enableAutomaticScroll
extraScrollHeight={120}
style={styles.keyboardScroll}
>
<View
style={styles.container}
onStartShouldSetResponder={() => {
Keyboard.dismiss();
return false;
}}
>
<LinearGradient <LinearGradient
colors={['#0f172a', '#1e293b', '#334155']} colors={['#0f172a', '#1e293b', '#334155']}
start={{ x: 0, y: 0 }} start={{ x: 0, y: 0 }}
@@ -374,38 +283,25 @@ export default function RegisterFormScreen() {
<AuthHeader /> <AuthHeader />
<SafeAreaView style={{ flex: 1 }} edges={['bottom']}> <SafeAreaView style={{ flex: 1 }} edges={['bottom']}>
<KeyboardAvoidingView <View style={styles.scrollContent}>
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
<ScrollView
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
>
<View style={styles.header}> <View style={styles.header}>
<View style={styles.iconContainer}> <View style={styles.iconContainer}>
<LinearGradient <LinearGradient
colors={headerInfo.gradient} colors={['#10b981', '#059669']}
style={styles.iconGradient} style={styles.iconGradient}
start={{ x: 0, y: 0 }} start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }} end={{ x: 1, y: 1 }}
> >
{headerInfo.icon} <UserPlus size={32} color="#fff" />
</LinearGradient> </LinearGradient>
</View> </View>
<Text style={styles.title}>{t(headerInfo.label)}</Text> <Text style={styles.title}>{t('Ro\'yxatdan o\'tish')}</Text>
<Text style={styles.subtitle}>
{isYattOrBand
? t("JSHSHR va passport ma'lumotlarini kiriting")
: t('INN raqamini kiriting')}
</Text>
</View> </View>
<View style={styles.card}> <View style={styles.card}>
<View style={styles.formGap}> <View style={styles.formGap}>
{/* ---- Davlat (Country) ---- */} <View>
{/* <View> <View>
<Text style={styles.label}>{t('Davlat')}</Text> <Text style={styles.label}>{t('Davlat')}</Text>
<TouchableOpacity <TouchableOpacity
style={[ style={[
@@ -429,7 +325,6 @@ export default function RegisterFormScreen() {
<Text <Text
style={[ style={[
styles.textInput, styles.textInput,
// { color: selectedCountry === 'all' ? '#94a3b8' : '#1e293b' },
{ color: '#94a3b8' }, { color: '#94a3b8' },
]} ]}
numberOfLines={1} numberOfLines={1}
@@ -440,147 +335,37 @@ export default function RegisterFormScreen() {
)} )}
<ChevronDown size={18} color={'#cbd5e1'} /> <ChevronDown size={18} color={'#cbd5e1'} />
</TouchableOpacity> </TouchableOpacity>
</View> */} </View>
{/* ---- YATT / BAND ---- */} </View>
{isYattOrBand && (
<>
<View> <View>
<Text style={styles.label}>{t('JSHSHR')}</Text> <Text style={styles.label}>{t('STIR')}</Text>
<View style={styles.input}> <View style={styles.input}>
<Hash size={18} color="#94a3b8" /> <Hash size={18} color="#94a3b8" />
<TextInput <TextInput
value={jshshr} value={stir}
keyboardType="numeric" keyboardType="numeric"
placeholder={t('JSHSHR kiriting (14 raqam)')} placeholder={t('STIR')}
placeholderTextColor="#94a3b8" placeholderTextColor="#94a3b8"
style={styles.textInput} style={{ flex: 1, color: "black" }}
onChangeText={handleJshshrChange} onChangeText={(text) => {
maxLength={14} const v = normalizeDigits(text).slice(0, 14);
testID="jshshr-input" setStir(v);
if (v.length === 9 || v.length === 14) {
setLoading(true);
mutate(v);
setRegionId(null)
setDistrictId(null)
setRegion(null)
setDistrict(null)
}
}}
/> />
{loading && jshshr.length >= 14 && ( {loading && <ActivityIndicator size="small" />}
<ActivityIndicator size="small" color="#3b82f6" />
)}
</View> </View>
</View> </View>
<View>
<Text style={styles.label}>{t('Passport seriya va raqami')}</Text>
<View style={styles.passportRow}>
<View style={[styles.input, styles.passportSeries]}>
<TextInput
value={passportSeries}
placeholder="AA"
placeholderTextColor="#94a3b8"
style={styles.textInput}
onChangeText={handlePassportSeriesChange}
maxLength={2}
autoCapitalize="characters"
testID="passport-series-input"
/>
</View>
<View style={[styles.input, styles.passportNumber]}>
<TextInput
value={passportNumber}
keyboardType="numeric"
placeholder="1234567"
placeholderTextColor="#94a3b8"
style={styles.textInput}
onChangeText={handlePassportNumberChange}
maxLength={7}
testID="passport-number-input"
/>
</View>
</View>
</View>
</>
)}
{/* ---- LEGAL ENTITY: INN ---- */}
{isLegal && (
<View>
<Text style={styles.label}>{t('INN')}</Text>
<View style={styles.input}>
<Hash size={18} color="#94a3b8" />
<TextInput
value={inn}
keyboardType="numeric"
placeholder={t('INN kiriting (9 raqam)')}
placeholderTextColor="#94a3b8"
style={styles.textInput}
onChangeText={handleInnChange}
maxLength={9}
testID="inn-input"
/>
{loading && inn.length >= 9 && (
<ActivityIndicator size="small" color="#3b82f6" />
)}
</View>
</View>
)}
{/* ---- LEGAL ENTITY: Kompaniya ma'lumoti ---- */}
{isLegal &&
info &&
(hasValidName ? (
<View style={styles.infoBox}>
<Text style={styles.infoLabel}>{t('Tashkilot')}</Text>
<Text style={styles.infoText}>{info.fullName || info.name}</Text>
</View>
) : hasValidInfo ? (
<View style={[styles.infoBox, { flexDirection: 'row', gap: 5 }]}>
<Text style={styles.infoLabel}>{t('Tashkilot')}</Text>
<Text style={styles.infoText}>{info.firstname}</Text>
<Text style={styles.infoText}>{info.lastname}</Text>
<Text style={styles.infoText}>{info.middlename}</Text>
</View>
) : (
<View style={styles.errorBox}>
<Text style={styles.errorText}>{t('Tashkilot topilmadi')}</Text>
</View>
))}
{/* ---- LEGAL ENTITY: Direktor JSHSHR — faqat info kelganda ko'rinadi ---- */}
{isLegal && info && (
<View>
<Text style={styles.label}>{t('Direktor JSHSHR')}</Text>
<View style={styles.input}>
<Hash size={18} color="#94a3b8" />
<TextInput
value={directorJshshr}
keyboardType="numeric"
placeholder={t('Direktor JSHSHR (14 raqam)')}
placeholderTextColor="#94a3b8"
style={styles.textInput}
onChangeText={handleDirectorJshshrChange}
maxLength={14}
testID="director-jshshr-input"
/>
{directorLoading && directorJshshr.length >= 14 && (
<ActivityIndicator size="small" color="#3b82f6" />
)}
</View>
{/* ---- Direktor ma'lumoti (INN mos kelgan holda) ---- */}
{directorInfo && (
<View style={[styles.infoBox, { marginTop: 8 }]}>
<Text style={styles.infoLabel}>{t('Direktor')}</Text>
<Text style={styles.infoText}>
{directorInfo?.data?.entrepreneur?.rows[0]?.entrepreneur}
</Text>
</View>
)}
{/* ---- Xato: INN mos kelmasa yoki server xatosi ---- */}
{directorInfoError && (
<View style={[styles.errorBox, { marginTop: 8 }]}>
<Text style={styles.errorText}>{directorInfoError}</Text>
</View>
)}
</View>
)}
{/* ---- Referal ---- */}
<View> <View>
<Text style={styles.label}>{t('Referal')}</Text> <Text style={styles.label}>{t('Referal')}</Text>
<View style={styles.input}> <View style={styles.input}>
@@ -591,12 +376,12 @@ export default function RegisterFormScreen() {
placeholderTextColor="#94a3b8" placeholderTextColor="#94a3b8"
style={styles.textInput} style={styles.textInput}
onChangeText={setReferal} onChangeText={setReferal}
maxLength={9}
testID="referal-input" testID="referal-input"
/> />
</View> </View>
</View> </View>
{/* ---- Telefon ---- */}
<View> <View>
<Text style={styles.label}>{t('Telefon raqami')}</Text> <Text style={styles.label}>{t('Telefon raqami')}</Text>
<View style={styles.input}> <View style={styles.input}>
@@ -606,48 +391,72 @@ export default function RegisterFormScreen() {
placeholder="90 123 45 67" placeholder="90 123 45 67"
placeholderTextColor="#94a3b8" placeholderTextColor="#94a3b8"
keyboardType="phone-pad" keyboardType="phone-pad"
style={styles.textInput} style={{ flex: 1 }}
onChangeText={(t) => setPhone(normalizeDigits(t).slice(0, 9))} onChangeText={(t) => setPhone(normalizeDigits(t))}
testID="phone-input"
/> />
</View> </View>
</View> </View>
{/* ---- YATT/BAND info ---- */} {hasDirectorTin && (
{!isLegal && <View>
info && <Text style={styles.label}>{t('Direktor STIR')}</Text>
(hasValidName ? ( <View style={[styles.input, { backgroundColor: isDirectorTinValid ? '#f0fdf4' : '#f8fafc' }]}>
<View style={styles.infoBox}> <Hash size={18} color="#94a3b8" />
<Text style={styles.infoText}>{info.fullName || info.name}</Text> <TextInput
value={directorTinInput}
keyboardType="numeric"
placeholder={t('Direktor STIR')}
placeholderTextColor="#94a3b8"
style={{ flex: 1 }}
maxLength={14}
onChangeText={(t) => setDirectorTinInput(normalizeDigits(t))}
/>
</View> </View>
) : hasValidInfo ? (
<View style={[styles.infoBox, { flexDirection: 'row', gap: 5 }]}>
<Text style={styles.infoText}>{info.firstname}</Text>
<Text style={styles.infoText}>{info.lastname}</Text>
<Text style={styles.infoText}>{info.middlename}</Text>
</View>
) : (
<View style={styles.errorBox}>
<Text style={styles.errorText}>{t('Foydalanuvchi topilmadi')}</Text>
</View>
))}
{/* ---- Davom etish tugmasi ---- */} {directorTinInput.length === 14 && !isDirectorTinValid && (
<Text style={styles.error}>{t('Direktor STIR notogri')}</Text>
)}
</View>
)}
{error !== null ?
<Text style={styles.notFound}>{t(error)}</Text>
: info && hasValidName &&
<Text style={styles.info}>{info.fullName || info.name}</Text>
}
<TouchableOpacity <TouchableOpacity
disabled={!isValid} disabled={!valid}
style={[styles.btn, !isValid && styles.disabled]} style={[styles.btn, !valid && styles.disabled]}
onPress={handleContinue} onPress={() => {
testID="continue-button" if (error === null) {
router.push({
pathname: '/(auth)/select-category',
params: {
phone,
first_name: stir.length === 9 ? info?.director : info?.fullName,
last_name: stir.length === 9 ? info?.director : info?.fullName,
company_name: info?.name,
district: districtId !== null ? districtId : regionId,
address: districtId !== null ? districtId : regionId,
director_full_name: stir.length === 9 ? info?.director : info?.fullName,
stir,
referal,
person_type: stir.length === 9 ? 'legal_entity' : info?.selfEmployment ? 'band' : info?.isItd ? 'ytt' : 'ytt',
},
})
}
}}
> >
<Text style={styles.btnText}>{t('Davom etish')}</Text> <Text style={styles.btnText}>{t('Davom etish')}</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
</ScrollView> </View>
</KeyboardAvoidingView>
</SafeAreaView> </SafeAreaView>
</View >
</KeyboardAwareScrollView >
{/* ---- Country BottomSheet ---- */}
<BottomSheet <BottomSheet
ref={countrySheetRef} ref={countrySheetRef}
index={-1} index={-1}
@@ -666,7 +475,6 @@ export default function RegisterFormScreen() {
<Text style={styles.sheetTitle}>{t('Davlat')}</Text> <Text style={styles.sheetTitle}>{t('Davlat')}</Text>
</View> </View>
{/* Search input */}
<View style={styles.searchContainer}> <View style={styles.searchContainer}>
<Search size={16} color="#94a3b8" /> <Search size={16} color="#94a3b8" />
<BottomSheetTextInput <BottomSheetTextInput
@@ -725,16 +533,20 @@ export default function RegisterFormScreen() {
); );
}} }}
/> />
</BottomSheet> </BottomSheet >
</View> </>
</View>
); );
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
keyboardScroll: {
flex: 1,
backgroundColor: '#0f172a',
},
container: { container: {
flex: 1, flex: 1,
backgroundColor: '#0f172a', backgroundColor: '#0f172a',
minHeight: '100%',
}, },
scrollContent: { scrollContent: {
flexGrow: 1, flexGrow: 1,
@@ -915,6 +727,7 @@ const styles = StyleSheet.create({
infoBox: { infoBox: {
backgroundColor: '#f0fdf4', backgroundColor: '#f0fdf4',
padding: 14, padding: 14,
marginTop: 10,
borderRadius: 14, borderRadius: 14,
borderWidth: 1, borderWidth: 1,
borderColor: '#bbf7d0', borderColor: '#bbf7d0',
@@ -964,15 +777,6 @@ const styles = StyleSheet.create({
fontWeight: '800' as const, fontWeight: '800' as const,
fontSize: 16, fontSize: 16,
}, },
footer: {
marginTop: 20,
alignItems: 'center',
},
footerText: {
color: '#94a3b8',
fontSize: 14,
fontWeight: '600' as const,
},
decorCircle1: { decorCircle1: {
position: 'absolute', position: 'absolute',
top: -150, top: -150,
@@ -992,7 +796,29 @@ const styles = StyleSheet.create({
backgroundColor: 'rgba(16, 185, 129, 0.08)', backgroundColor: 'rgba(16, 185, 129, 0.08)',
}, },
inputDisabled: { inputDisabled: {
backgroundColor: '#f1f5f9', // disabled bo'lganda fon rangi backgroundColor: '#f1f5f9',
borderColor: '#e2e8f0', // disabled bo'lganda border rangi borderColor: '#e2e8f0',
},
info: {
padding: 12,
borderRadius: 12,
fontWeight: '700',
backgroundColor: '#f0fdf4',
},
error: {
color: '#dc2626',
fontSize: 12,
marginTop: 4,
fontWeight: '600',
},
notFound: {
backgroundColor: '#fef2f2',
padding: 12,
borderRadius: 12,
fontWeight: '700',
color: '#dc2626',
borderWidth: 1,
borderColor: '#fecaca',
}, },
}); });

View File

@@ -1,6 +1,6 @@
import createContextHook from '@nkzw/create-context-hook'; import createContextHook from '@nkzw/create-context-hook';
import { useState } from 'react'; import { useState } from 'react';
import { GetDirectorInfoResponse } from '../../login/lib/api'; import { GetInfo } from '../../login/lib/api';
export type PersonType = 'yatt' | 'band' | 'legal_entity' | null; export type PersonType = 'yatt' | 'band' | 'legal_entity' | null;
@@ -19,14 +19,14 @@ interface RegisterState {
setPassportNumber: (number: string) => void; setPassportNumber: (number: string) => void;
inn: string; inn: string;
setInn: (inn: string) => void; setInn: (inn: string) => void;
info: any; info: GetInfo | null;
setInfo: (info: any) => void; setInfo: (info: GetInfo | null) => void;
reset_full: () => void; reset_full: () => void;
reset: () => void; reset: () => void;
directorJshshr: string; directorJshshr: string;
setDirectorJshshr: (directorJshshr: string) => void; setDirectorJshshr: (directorJshshr: string) => void;
directorInfo: GetDirectorInfoResponse | null; directorInfo: GetInfo | null;
setDirectorInfo: (directorInfo: GetDirectorInfoResponse | null) => void; setDirectorInfo: (directorInfo: GetInfo | null) => void;
} }
export const [RegisterProvider, useRegister] = createContextHook<RegisterState>(() => { export const [RegisterProvider, useRegister] = createContextHook<RegisterState>(() => {
@@ -37,9 +37,9 @@ export const [RegisterProvider, useRegister] = createContextHook<RegisterState>(
const [passportSeries, setPassportSeries] = useState<string>(''); const [passportSeries, setPassportSeries] = useState<string>('');
const [passportNumber, setPassportNumber] = useState<string>(''); const [passportNumber, setPassportNumber] = useState<string>('');
const [inn, setInn] = useState<string>(''); const [inn, setInn] = useState<string>('');
const [info, setInfo] = useState<any>(null); const [info, setInfo] = useState<GetInfo | null>(null);
const [directorJshshr, setDirectorJshshr] = useState<string>(''); const [directorJshshr, setDirectorJshshr] = useState<string>('');
const [directorInfo, setDirectorInfo] = useState<GetDirectorInfoResponse | null>(null); const [directorInfo, setDirectorInfo] = useState<GetInfo | null>(null);
const reset_full = () => { const reset_full = () => {
setPersonType(null); setPersonType(null);

View File

@@ -7,18 +7,19 @@ import React, { useCallback, useRef, useState } from 'react';
import { import {
Alert, Alert,
Image, Image,
KeyboardAvoidingView, Keyboard,
Linking, Linking,
ScrollView,
StyleSheet, StyleSheet,
Text, Text,
TouchableOpacity, TouchableOpacity,
View, TouchableWithoutFeedback,
View
} from 'react-native'; } from 'react-native';
import OneClick from '@/assets/images/one_click.png'; import OneClick from '@/assets/images/one_click.png';
import PAYME from '@/assets/images/Payme_NEW.png'; import PAYME from '@/assets/images/Payme_NEW.png';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { KeyboardAwareScrollView, } from 'react-native-keyboard-aware-scroll-view';
import { price_calculation } from '../lib/api'; import { price_calculation } from '../lib/api';
import { CreateAdsResponse } from '../lib/types'; import { CreateAdsResponse } from '../lib/types';
import StepFour from './StepFour'; import StepFour from './StepFour';
@@ -185,13 +186,16 @@ export default function CreateAdsScreens() {
onSuccess: async (res, variables) => { onSuccess: async (res, variables) => {
if (variables.paymentType === 'payme') { if (variables.paymentType === 'payme') {
await Linking.openURL(res.data.url); await Linking.openURL(res.data.url);
router.push('/(dashboard)/announcements'); bottomSheetModalRef.current?.dismiss();
router.push('/profile/my-ads');
} else { } else {
router.push('/(dashboard)/announcements'); bottomSheetModalRef.current?.dismiss();
router.push('/profile/my-ads')
} }
}, },
onError: (err) => { onError: (err: AxiosError) => {
Alert.alert('Xatolik yuz berdi', err.message); const errMessage = (err.response?.data as { referral_amount: string }).referral_amount
Alert.alert(t('Xatolik yuz berdi'), errMessage || err.message);
}, },
}); });
@@ -206,11 +210,19 @@ export default function CreateAdsScreens() {
}; };
return ( return (
<KeyboardAvoidingView <KeyboardAwareScrollView
behavior="padding" showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
style={[styles.safeArea, isDark ? styles.darkBg : styles.lightBg]} style={[styles.safeArea, isDark ? styles.darkBg : styles.lightBg]}
> >
<ScrollView contentContainerStyle={[styles.container, { paddingBottom: 90 }]}> <TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View
style={[styles.container, { marginBottom: Keyboard.isVisible() ? 10 : 90 }]}
onStartShouldSetResponder={() => {
Keyboard.dismiss();
return false;
}}
>
<Image <Image
source={OneClick} source={OneClick}
style={{ width: 180, height: 56, marginBottom: 10 }} style={{ width: 180, height: 56, marginBottom: 10 }}
@@ -273,11 +285,8 @@ export default function CreateAdsScreens() {
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</ScrollView> </View>
</TouchableWithoutFeedback>
{/* FOOTER */}
{/* PAYMENT BOTTOM SHEET */}
<BottomSheetModal <BottomSheetModal
ref={bottomSheetModalRef} ref={bottomSheetModalRef}
index={0} index={0}
@@ -321,7 +330,7 @@ export default function CreateAdsScreens() {
</View> </View>
</BottomSheetScrollView> </BottomSheetScrollView>
</BottomSheetModal> </BottomSheetModal>
</KeyboardAvoidingView> </KeyboardAwareScrollView>
); );
} }

View File

@@ -234,10 +234,10 @@ const StepThree = forwardRef(({ formData, updateForm, data }: StepProps, ref) =>
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<Text style={[styles.sectionTitle, { color: theme.text }]}> <Text style={[styles.sectionTitle, { color: theme.text }]}>
{t('Reklama joylashtirish kompaniyasi')} {t('Reklama joylashtirish kompaniyasi')}
</Text> </Text>
<View style={{ flexDirection: 'column', justifyContent: 'space-between', alignItems: 'flex-end', marginBottom: 10 }}>
<TouchableOpacity <TouchableOpacity
style={[ style={[
styles.selectAllButton, styles.selectAllButton,
@@ -330,7 +330,7 @@ const styles = StyleSheet.create({
marginBottom: 12, marginBottom: 12,
}, },
pickerText: { fontSize: 16 }, pickerText: { fontSize: 16 },
sectionTitle: { fontSize: 16, fontWeight: '700', marginVertical: 12 }, sectionTitle: { fontSize: 16, fontWeight: '700', marginVertical: 12, },
companyItem: { companyItem: {
width: 55, width: 55,
height: 55, height: 55,

View File

@@ -3,7 +3,8 @@ import CategorySelection from '@/components/ui/IndustrySelection';
import { XIcon } from 'lucide-react-native'; import { XIcon } from 'lucide-react-native';
import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react'; import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FlatList, StyleSheet, Text, ToastAndroid, TouchableOpacity, View } from 'react-native'; import { FlatList, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { Toast } from 'toastify-react-native';
type StepProps = { type StepProps = {
formData: any; formData: any;
@@ -36,7 +37,7 @@ const StepTwo = forwardRef(({ formData, updateForm }: StepProps, ref) => {
const validate = () => { const validate = () => {
if (selectedCategories.length === 0) { if (selectedCategories.length === 0) {
setError('Iltimos, kompaniyalarni tanlang'); setError('Iltimos, kompaniyalarni tanlang');
ToastAndroid.show(t('Iltimos, kompaniyalarni tanlang'), ToastAndroid.TOP); Toast.info(t('Iltimos, kompaniyalarni tanlang'));
return false; return false;
} }
return true; return true;

View File

@@ -3,19 +3,19 @@ import { useTheme } from "@/components/ThemeContext";
import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useQuery, useQueryClient } from "@tanstack/react-query";
import { Image } from "expo-image"; import { Image } from "expo-image";
import { router } from "expo-router"; import { router } from "expo-router";
import * as WebBrowser from "expo-web-browser";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next";
import { import {
ActivityIndicator, ActivityIndicator,
FlatList, FlatList,
Text, Text,
ToastAndroid,
TouchableOpacity, TouchableOpacity,
View, View
} from "react-native"; } from "react-native";
import { RefreshControl } from "react-native-gesture-handler"; import { RefreshControl } from "react-native-gesture-handler";
import * as WebBrowser from "expo-web-browser"; import { Toast } from "toastify-react-native";
import { eservices_api } from "../lib/api"; import { eservices_api } from "../lib/api";
import { useTranslation } from "react-i18next";
const dark = { const dark = {
bg: "#0f172a", bg: "#0f172a",
@@ -39,11 +39,14 @@ export default function EServicesCategoryScreen() {
const handleOpenBrowser = async (fileUrl: string) => { const handleOpenBrowser = async (fileUrl: string) => {
try { try {
await WebBrowser.openBrowserAsync(fileUrl); await WebBrowser.openBrowserAsync(fileUrl, {
dismissButtonStyle: 'close',
presentationStyle: WebBrowser.WebBrowserPresentationStyle.FULL_SCREEN,
});
} catch (error) { } catch (error) {
ToastAndroid.show(t("Xatolik yuz berdi"), ToastAndroid.TOP); Toast.error(t("Xatolik yuz berdi"));
}
} }
};
const onRefresh = async () => { const onRefresh = async () => {
setRefreshing(true); setRefreshing(true);

View File

@@ -2,6 +2,7 @@ import { useTheme } from "@/components/ThemeContext";
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
import { Image } from "expo-image"; import { Image } from "expo-image";
import { router, useLocalSearchParams } from "expo-router"; import { router, useLocalSearchParams } from "expo-router";
import * as WebBrowser from "expo-web-browser";
import { ChevronLeft } from "lucide-react-native"; import { ChevronLeft } from "lucide-react-native";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { import {
@@ -10,14 +11,13 @@ import {
FlatList, FlatList,
StyleSheet, StyleSheet,
Text, Text,
ToastAndroid,
TouchableOpacity, TouchableOpacity,
View, View
} from "react-native"; } from "react-native";
import * as WebBrowser from "expo-web-browser";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { RefreshControl } from "react-native-gesture-handler"; import { RefreshControl } from "react-native-gesture-handler";
import { Toast } from "toastify-react-native";
import { eservices_api } from "../lib/api"; import { eservices_api } from "../lib/api";
const { width: SCREEN_WIDTH } = Dimensions.get("window"); const { width: SCREEN_WIDTH } = Dimensions.get("window");
@@ -74,11 +74,10 @@ export default function EServicesScreen() {
}; };
const handleOpenBrowser = async (fileUrl: string) => { const handleOpenBrowser = async (fileUrl: string) => {
ToastAndroid.show(t("Xatolik yuz berdi"), ToastAndroid.TOP);
try { try {
await WebBrowser.openBrowserAsync(fileUrl); await WebBrowser.openBrowserAsync(fileUrl);
} catch (error) { } catch (error) {
ToastAndroid.show(t("Xatolik yuz berdi"), ToastAndroid.TOP); Toast.error(t("Xatolik yuz berdi"));
} }
}; };

View File

@@ -57,7 +57,6 @@ export default function HomeScreen() {
} }
await queryClient.refetchQueries(); await queryClient.refetchQueries();
} catch (err) { } catch (err) {
console.error('Refresh error:', err);
} finally { } finally {
setRefreshing(false); setRefreshing(false);
} }

View File

@@ -61,10 +61,6 @@ export const user_api = {
return res; return res;
}, },
async my_ads_detail(id: number): Promise<AxiosResponse<{ status: boolean; data: MyAdsDataRes }>> {
const res = await httpClient.get(API_URLS.My_Ads_Detail(id));
return res;
},
async my_bonuses(params: { async my_bonuses(params: {
page: number; page: number;
@@ -109,6 +105,11 @@ export const user_api = {
return res; return res;
}, },
async ads_detail(id: number): Promise<AxiosResponse<{ status: boolean; data: MyAdsDataRes }>> {
const res = await httpClient.get(API_URLS.Ads_Detail(id));
return res;
},
async my_referrals(params: { page: number; page_size: number }) { async my_referrals(params: { page: number; page_size: number }) {
const res = await httpClient.get(API_URLS.My_Refferals, { params }); const res = await httpClient.get(API_URLS.My_Refferals, { params });
return res; return res;

View File

@@ -3,7 +3,7 @@ import { formatPhone, normalizeDigits } from '@/constants/formatPhone';
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import { ArrowLeft } from 'lucide-react-native'; import { ArrowLeft, Check } from 'lucide-react-native';
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
@@ -14,9 +14,9 @@ import {
StyleSheet, StyleSheet,
Text, Text,
TextInput, TextInput,
ToastAndroid, View
View,
} from 'react-native'; } from 'react-native';
import { Toast } from 'toastify-react-native';
import { user_api } from '../lib/api'; import { user_api } from '../lib/api';
export default function AddEmployee() { export default function AddEmployee() {
@@ -60,14 +60,14 @@ export default function AddEmployee() {
const handleSave = () => { const handleSave = () => {
if (!firstName.trim() || !lastName.trim() || !phoneNumber.trim()) { if (!firstName.trim() || !lastName.trim() || !phoneNumber.trim()) {
ToastAndroid.show(t("Barcha maydonlarni to'ldiring"), ToastAndroid.SHORT); Toast.info(t("Barcha maydonlarni to'ldiring"));
return; return;
} }
mutate({ mutate({
first_name: firstName.trim(), first_name: firstName.trim(),
last_name: lastName.trim(), last_name: lastName.trim(),
phone: phoneNumber.trim(), phone: `998${phoneNumber.trim()}`,
}); });
}; };
@@ -77,12 +77,12 @@ export default function AddEmployee() {
<Pressable onPress={() => router.push('/profile/employees')}> <Pressable onPress={() => router.push('/profile/employees')}>
<ArrowLeft color={theme.text} /> <ArrowLeft color={theme.text} />
</Pressable> </Pressable>
<Text style={[styles.headerTitle, { color: theme.text }]}>{t("Yangi xodim qo'shish")}</Text> <Text style={[styles.headerTitle, { color: theme.text }]}>{t("Xodim qo'shish")}</Text>
<Pressable onPress={handleSave} disabled={isPending}> <Pressable onPress={handleSave} disabled={isPending}>
{isPending ? ( {isPending ? (
<ActivityIndicator size={'small'} /> <ActivityIndicator size={'small'} />
) : ( ) : (
<Text style={[styles.saveButton, { color: theme.primary }]}>{t('Saqlash')}</Text> <Check size={26} color={theme.primary} />
)} )}
</Pressable> </Pressable>
</View> </View>

View File

@@ -52,7 +52,7 @@ export function AnnouncementsTab() {
}; };
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
const [selectedAnnouncement, setSelectedAnnouncement] = useState<MyAdsDataRes | null>(null); const [selectedAnnouncement, setSelectedAnnouncement] = useState<number | null>(null);
const [sheetOpen, setSheetOpen] = useState(false); const bottomSheetModalRef = useRef<BottomSheetModal>(null); const [sheetOpen, setSheetOpen] = useState(false); const bottomSheetModalRef = useRef<BottomSheetModal>(null);
const { data, isLoading, isError, fetchNextPage, hasNextPage, refetch } = useInfiniteQuery({ const { data, isLoading, isError, fetchNextPage, hasNextPage, refetch } = useInfiniteQuery({
@@ -82,14 +82,14 @@ export function AnnouncementsTab() {
isLoading: loadingDetail, isLoading: loadingDetail,
isError: detailError, isError: detailError,
} = useQuery({ } = useQuery({
queryKey: ['my_ads_id', selectedAnnouncement?.id], queryKey: ['my_ads_id', selectedAnnouncement],
queryFn: () => user_api.my_ads_detail(selectedAnnouncement?.id!), queryFn: () => user_api.ads_detail(Number(selectedAnnouncement)),
select: (res) => res.data.data, select: (res) => res.data.data,
enabled: !!selectedAnnouncement && sheetOpen, enabled: !!selectedAnnouncement,
}); });
const openSheet = (item: MyAdsDataRes) => { const openSheet = (item: MyAdsDataRes) => {
setSelectedAnnouncement(item); setSelectedAnnouncement(item.id);
setSheetOpen(true); setSheetOpen(true);
requestAnimationFrame(() => bottomSheetRef.current?.present()); requestAnimationFrame(() => bottomSheetRef.current?.present());
}; };
@@ -176,7 +176,7 @@ export function AnnouncementsTab() {
if (isError) { if (isError) {
return ( return (
<View style={[styles.center, { backgroundColor: theme.background }]}> <View style={[styles.center, { backgroundColor: theme.background }]}>
<Text style={[styles.error, { color: theme.error }]}>{t('Xatolik yuz berdi')}</Text> <Text style={[{ color: theme.error }]}>{t('Xatolik yuz berdi')}</Text>
</View> </View>
); );
} }
@@ -196,7 +196,7 @@ export function AnnouncementsTab() {
<FlatList <FlatList
data={allAds} data={allAds}
keyExtractor={(item) => item.id.toString()} keyExtractor={(item) => item.id.toString()}
contentContainerStyle={styles.list} contentContainerStyle={[styles.list, { flexGrow: 1 }]}
refreshControl={ refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={theme.primary} /> <RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={theme.primary} />
} }
@@ -204,7 +204,7 @@ export function AnnouncementsTab() {
renderItem={({ item }) => ( renderItem={({ item }) => (
<Pressable <Pressable
style={[styles.card, { backgroundColor: theme.cardBg }]} style={[styles.card, { backgroundColor: theme.cardBg }]}
onPress={() => openSheet(item)} onPress={() => { openSheet(item); setSelectedAnnouncement(item.id) }}
> >
{item.files?.[0]?.file && ( {item.files?.[0]?.file && (
<Image source={{ uri: item.files[0].file }} style={styles.cardImage} /> <Image source={{ uri: item.files[0].file }} style={styles.cardImage} />
@@ -243,6 +243,14 @@ export function AnnouncementsTab() {
</View> </View>
</Pressable> </Pressable>
)} )}
ListEmptyComponent={() => (
<View style={styles.emptyContainer}>
<Megaphone size={48} color={theme.textSecondary} />
<Text style={[styles.emptyTitle, { color: theme.text }]}>
{t('Hozircha hech qanday eʼlon mavjud emas')}
</Text>
</View>
)}
/> />
<BottomSheetModal <BottomSheetModal
@@ -253,14 +261,13 @@ export function AnnouncementsTab() {
backgroundStyle={{ backgroundColor: theme.sheetBg }} backgroundStyle={{ backgroundColor: theme.sheetBg }}
handleIndicatorStyle={{ backgroundColor: theme.indicator }} handleIndicatorStyle={{ backgroundColor: theme.indicator }}
onDismiss={() => { onDismiss={() => {
setSheetOpen(false); setSelectedAnnouncement(null); // shu yetarli
setSelectedAnnouncement(null);
}} }}
> >
<BottomSheetScrollView contentContainerStyle={styles.sheet}> <BottomSheetScrollView contentContainerStyle={styles.sheet}>
{loadingDetail && <ActivityIndicator size={'large'} />} {loadingDetail && <ActivityIndicator size={'large'} />}
{detailError && ( {detailError && (
<Text style={[styles.error, { color: theme.error }]}>{t('Xatolik yuz berdi')}</Text> <Text style={[{ color: theme.error }]}>{t('Xatolik yuz berdi')}</Text>
)} )}
{detail && ( {detail && (
@@ -374,7 +381,7 @@ export function AnnouncementsTab() {
isDark ? styles.darkPaymentItem : styles.lightPaymentItem, isDark ? styles.darkPaymentItem : styles.lightPaymentItem,
{ flexDirection: 'row', justifyContent: 'flex-start', alignItems: 'center' }, { flexDirection: 'row', justifyContent: 'flex-start', alignItems: 'center' },
]} ]}
onPress={() => sendPayment({ id: selectedAnnouncement?.id!, type: 'payme' })} onPress={() => sendPayment({ id: selectedAnnouncement!, type: 'payme' })}
> >
<Image source={PAYME} style={{ width: 80, height: 80 }} /> <Image source={PAYME} style={{ width: 80, height: 80 }} />
</TouchableOpacity> </TouchableOpacity>
@@ -384,7 +391,7 @@ export function AnnouncementsTab() {
styles.paymentItem, styles.paymentItem,
isDark ? styles.darkPaymentItem : styles.lightPaymentItem, isDark ? styles.darkPaymentItem : styles.lightPaymentItem,
]} ]}
onPress={() => sendPayment({ id: selectedAnnouncement?.id!, type: 'referral' })} onPress={() => sendPayment({ id: selectedAnnouncement!, type: 'referral' })}
> >
<Text style={[styles.paymentText, isDark ? styles.darkText : styles.lightText]}> <Text style={[styles.paymentText, isDark ? styles.darkText : styles.lightText]}>
{t('Referal orqali')} {t('Referal orqali')}
@@ -498,6 +505,22 @@ const styles = StyleSheet.create({
fontWeight: '700', fontWeight: '700',
}, },
loading: {}, emptyContainer: {
error: {}, flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 60,
},
emptyTitle: {
fontSize: 18,
fontWeight: '600',
marginTop: 12,
},
emptyDesc: {
fontSize: 14,
marginTop: 6,
textAlign: 'center',
},
}); });

View File

@@ -164,6 +164,7 @@ const styles = StyleSheet.create({
padding: 16, padding: 16,
gap: 16, gap: 16,
paddingBottom: 30, paddingBottom: 30,
flexGrow: 1
}, },
card: { card: {
borderRadius: 20, borderRadius: 20,
@@ -236,6 +237,7 @@ const styles = StyleSheet.create({
fontWeight: '600' as const, fontWeight: '600' as const,
}, },
emptyContainer: { emptyContainer: {
flex: 1,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
paddingVertical: 80, paddingVertical: 80,

View File

@@ -12,9 +12,9 @@ import {
Switch, Switch,
Text, Text,
TextInput, TextInput,
ToastAndroid, View
View,
} from 'react-native'; } from 'react-native';
import { Toast } from 'toastify-react-native';
import { user_api } from '../lib/api'; import { user_api } from '../lib/api';
type FormType = { type FormType = {
@@ -44,12 +44,12 @@ export default function CreateReferrals() {
is_agent: boolean; is_agent: boolean;
}) => user_api.create_referral(body), }) => user_api.create_referral(body),
onSuccess: () => { onSuccess: () => {
ToastAndroid.show(t('Referral yaratildi'), ToastAndroid.SHORT); Toast.success(t('Referral yaratildi'));
queryClient.refetchQueries({ queryKey: ['my_referrals'] }); queryClient.refetchQueries({ queryKey: ['my_referrals'] });
router.back(); router.back();
}, },
onError: () => { onError: () => {
ToastAndroid.show(t('Xatolik yuz berdi'), ToastAndroid.SHORT); Toast.error(t('Xatolik yuz berdi'));
}, },
}); });
@@ -60,8 +60,6 @@ export default function CreateReferrals() {
const validate = () => { const validate = () => {
const e: any = {}; const e: any = {};
if (!form.code || form.code.length !== 9)
e.code = 'Kod aynan 9 ta belgidan iborat bolishi kerak';
if (!form.description || form.description.length < 5) if (!form.description || form.description.length < 5)
e.description = 'Tavsif kamida 5 ta belgidan iborat bolishi kerak'; e.description = 'Tavsif kamida 5 ta belgidan iborat bolishi kerak';

View File

@@ -86,13 +86,11 @@ export default function EditService() {
const { mutate, isPending } = useMutation({ const { mutate, isPending } = useMutation({
mutationFn: (body: FormData) => user_api.update_service({ body, id: Number(id) }), mutationFn: (body: FormData) => user_api.update_service({ body, id: Number(id) }),
onSuccess: (res) => { onSuccess: (res) => {
console.log(res);
queryClient.invalidateQueries({ queryKey: ['my_services'] }); queryClient.invalidateQueries({ queryKey: ['my_services'] });
queryClient.invalidateQueries({ queryKey: ['service_detail'] }); queryClient.invalidateQueries({ queryKey: ['service_detail'] });
router.back(); router.back();
}, },
onError: (err: any) => { onError: (err: any) => {
console.log(err);
Alert.alert(t('Xatolik yuz berdi'), err?.message || t('Yangilashda xato yuz berdi')); Alert.alert(t('Xatolik yuz berdi'), err?.message || t('Yangilashda xato yuz berdi'));
}, },
}); });
@@ -151,7 +149,7 @@ export default function EditService() {
<ArrowLeft color={isDark ? '#fff' : '#0f172a'} /> <ArrowLeft color={isDark ? '#fff' : '#0f172a'} />
</Pressable> </Pressable>
<Text style={[styles.headerTitle, { color: isDark ? '#fff' : '#0f172a' }]}> <Text style={[styles.headerTitle, { color: isDark ? '#fff' : '#0f172a' }]}>
{step === 1 ? t('Xizmatni tahrirlash (1/2)') : t('Xizmatni tahrirlash (2/2)')} {t('Xizmatni tahrirlash')}
</Text> </Text>
</View> </View>
<Pressable <Pressable

View File

@@ -124,7 +124,7 @@ export function EmployeesTab() {
data={allEmployees} data={allEmployees}
keyExtractor={(item) => item.phone} keyExtractor={(item) => item.phone}
renderItem={renderItem} renderItem={renderItem}
contentContainerStyle={styles.list} contentContainerStyle={[styles.list, { flexGrow: 1 }]}
onEndReached={() => { onEndReached={() => {
if (hasNextPage && !isFetchingNextPage) { if (hasNextPage && !isFetchingNextPage) {
fetchNextPage(); fetchNextPage();
@@ -176,7 +176,12 @@ const styles = StyleSheet.create({
infoContainer: { flex: 1, gap: 4 }, infoContainer: { flex: 1, gap: 4 },
name: { fontSize: 17, fontWeight: '700' }, name: { fontSize: 17, fontWeight: '700' },
phone: { fontSize: 15, fontWeight: '500' }, phone: { fontSize: 15, fontWeight: '500' },
emptyContainer: { alignItems: 'center', justifyContent: 'center', paddingVertical: 80, gap: 16 }, emptyContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 60,
},
emptyText: { fontSize: 17, fontWeight: '600' }, emptyText: { fontSize: 17, fontWeight: '600' },
emptyButton: { emptyButton: {
flexDirection: 'row', flexDirection: 'row',

View File

@@ -68,9 +68,9 @@ export function ManualTab() {
/** 🔹 Video manbalari (SELECT ga bogliq) */ /** 🔹 Video manbalari (SELECT ga bogliq) */
const videos = { const videos = {
uz: require('@/assets/manual/manual_video_uz.webm'), uz: require('@/assets/manual/manual_video_uz.mp4'),
ru: require('@/assets/manual/manual_video_ru.webm'), ru: require('@/assets/manual/manual_video_ru.mp4'),
en: require('@/assets/manual/manual_video_en.webm'), en: require('@/assets/manual/manual_video_en.mp4'),
}; };
const player = useVideoPlayer(videos[selectedLang], (player) => { const player = useVideoPlayer(videos[selectedLang], (player) => {
@@ -158,8 +158,6 @@ export function ManualTab() {
[imageLang] [imageLang]
); );
const selectedLanguage = languages.find((l) => l.code === selectedLang);
useEffect(() => { useEffect(() => {
// listener qo'shish // listener qo'shish
const subscription = player.addListener('playingChange', (state) => { const subscription = player.addListener('playingChange', (state) => {
@@ -350,7 +348,7 @@ const styles = StyleSheet.create({
container: { flex: 1 }, container: { flex: 1 },
hero: { padding: 20 }, hero: { padding: 20 },
topHeader: { flexDirection: 'row', alignItems: 'center', gap: 12 }, topHeader: { flexDirection: 'row', alignItems: 'center', gap: 12 },
headerTitle: { fontSize: 22, fontWeight: '700', marginHorizontal: 16 }, headerTitle: { fontSize: 18, fontWeight: '700', marginHorizontal: 16 },
subtitle: { fontSize: 16, marginTop: 5, fontWeight: '500', marginHorizontal: 16 }, subtitle: { fontSize: 16, marginTop: 5, fontWeight: '500', marginHorizontal: 16 },
section: { marginBottom: 28 }, section: { marginBottom: 28 },

View File

@@ -116,7 +116,7 @@ export default function MyServicesScreen() {
<FlatList <FlatList
data={allServices} data={allServices}
keyExtractor={(item) => item.id.toString()} keyExtractor={(item) => item.id.toString()}
contentContainerStyle={styles.list} contentContainerStyle={[styles.list, { flexGrow: 1 }]}
onEndReached={() => hasNextPage && fetchNextPage()} onEndReached={() => hasNextPage && fetchNextPage()}
refreshControl={ refreshControl={
<RefreshControl <RefreshControl
@@ -269,7 +269,7 @@ const styles = StyleSheet.create({
borderRadius: 12, borderRadius: 12,
}, },
categoryText: { fontSize: 13, fontWeight: '500' as const }, categoryText: { fontSize: 13, fontWeight: '500' as const },
emptyContainer: { alignItems: 'center', justifyContent: 'center', paddingVertical: 80, gap: 16 }, emptyContainer: { alignItems: 'center', justifyContent: 'center', paddingVertical: 80, gap: 16, flex: 1 },
emptyText: { fontSize: 17, fontWeight: '600' as const }, emptyText: { fontSize: 17, fontWeight: '600' as const },
emptyButton: { emptyButton: {
flexDirection: 'row', flexDirection: 'row',

View File

@@ -91,12 +91,47 @@ export function NotificationTab() {
<Text style={[styles.headerTitle, { color: isDark ? '#f1f5f9' : '#0f172a' }]}> <Text style={[styles.headerTitle, { color: isDark ? '#f1f5f9' : '#0f172a' }]}>
{t('Bildirishnomalar')} {t('Bildirishnomalar')}
</Text> </Text>
</View>
<FlatList
data={notifications}
keyExtractor={(item) => item.id.toString()}
contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
ListHeaderComponent={() => {
if (notifications.length === 0) {
return (
<View style={styles.emptyHeader}>
<Text
style={[
styles.emptyTitle,
{ color: isDark ? '#f1f5f9' : '#0f172a' },
]}
>
{t("Hozircha bildirishnomalar yo'q")}
</Text>
{notifications.some((n) => !n.is_read) && ( <Text
style={[
styles.emptyDesc,
{ color: isDark ? '#94a3b8' : '#64748b' },
]}
>
{t("Yangi xabarlar shu yerda paydo boladi")}
</Text>
</View>
);
}
if (notifications.some((n) => !n.is_read)) {
return (
<View style={styles.headerActions}>
<TouchableOpacity <TouchableOpacity
style={[ style={[
styles.markAllButton, styles.markAllButton,
{ backgroundColor: isDark ? '#1e293b' : '#e0f2fe', borderColor: '#3b82f6' }, {
backgroundColor: isDark ? '#1e293b' : '#e0f2fe',
borderColor: '#3b82f6',
},
]} ]}
onPress={() => markAllAsRead()} onPress={() => markAllAsRead()}
disabled={isMarkingAllRead} disabled={isMarkingAllRead}
@@ -109,14 +144,24 @@ export function NotificationTab() {
</Text> </Text>
)} )}
</TouchableOpacity> </TouchableOpacity>
)}
</View> </View>
<FlatList );
data={notifications} }
keyExtractor={(item) => item.id.toString()}
contentContainerStyle={styles.listContent} return (
showsVerticalScrollIndicator={false} <View style={styles.allReadContainer}>
renderItem={({ item, index }) => <NotificationCard item={item} />} <Text
style={[
styles.allReadText,
{ color: isDark ? '#94a3b8' : '#64748b' },
]}
>
{t("Barcha bildirishnomalar oqilgan")}
</Text>
</View>
);
}}
renderItem={({ item }) => <NotificationCard item={item} />}
onEndReached={() => { onEndReached={() => {
if (hasNextPage && !isFetchingNextPage) { if (hasNextPage && !isFetchingNextPage) {
fetchNextPage(); fetchNextPage();
@@ -130,6 +175,15 @@ export function NotificationTab() {
} }
refreshing={isLoading} refreshing={isLoading}
onRefresh={refetch} onRefresh={refetch}
ListEmptyComponent={() =>
!isLoading && (
<View style={styles.emptyContainer}>
<Text style={[styles.emptyTitle, { color: isDark ? '#f1f5f9' : '#0f172a' }]}>
{t("Hozircha bildirishnomalar yo'q")}
</Text>
</View>
)
}
/> />
</View> </View>
); );
@@ -266,6 +320,7 @@ const styles = StyleSheet.create({
listContent: { listContent: {
padding: 16, padding: 16,
paddingBottom: 32, paddingBottom: 32,
flexGrow: 1
}, },
/* Card Styles */ /* Card Styles */
@@ -292,6 +347,38 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center',
}, },
headerActions: {
marginBottom: 16,
alignItems: 'flex-end',
},
emptyHeader: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 60,
},
allReadContainer: {
marginBottom: 16,
alignItems: 'center',
},
allReadText: {
fontSize: 14,
},
emptyTitle: {
fontSize: 18,
fontWeight: '700',
marginBottom: 8,
textAlign: 'center',
},
emptyDesc: {
fontSize: 14,
textAlign: 'center',
},
cardHeader: { cardHeader: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
@@ -407,7 +494,7 @@ const styles = StyleSheet.create({
paddingVertical: 8, paddingVertical: 8,
borderRadius: 8, borderRadius: 8,
borderWidth: 1, borderWidth: 1,
minWidth: 80, width: "auto",
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}, },
@@ -415,4 +502,10 @@ const styles = StyleSheet.create({
fontSize: 13, fontSize: 13,
fontWeight: '600', fontWeight: '600',
}, },
emptyContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 30,
},
}); });

View File

@@ -313,6 +313,7 @@ const styles = StyleSheet.create({
}, },
list: { list: {
gap: 16, gap: 16,
flexGrow: 1
}, },
card: { card: {
backgroundColor: '#1e293b', backgroundColor: '#1e293b',
@@ -372,6 +373,7 @@ const styles = StyleSheet.create({
fontWeight: '500' as const, fontWeight: '500' as const,
}, },
emptyContainer: { emptyContainer: {
flex: 1,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
paddingVertical: 80, paddingVertical: 80,

View File

@@ -2,7 +2,7 @@ import { useTheme } from '@/components/ThemeContext';
import { useInfiniteQuery } from '@tanstack/react-query'; import { useInfiniteQuery } from '@tanstack/react-query';
import * as Clipboard from 'expo-clipboard'; import * as Clipboard from 'expo-clipboard';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import { ArrowLeft, CopyIcon, HandCoins, Plus, Users } from 'lucide-react-native'; import { ArrowLeft, CopyIcon, Gift, HandCoins, Plus, Users } from 'lucide-react-native';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
@@ -14,11 +14,11 @@ import {
Share, Share,
StyleSheet, StyleSheet,
Text, Text,
ToastAndroid,
TouchableOpacity, TouchableOpacity,
View, View
} from 'react-native'; } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { Toast } from 'toastify-react-native';
import { user_api } from '../lib/api'; import { user_api } from '../lib/api';
const PAGE_SIZE = 10; const PAGE_SIZE = 10;
@@ -68,7 +68,7 @@ export function ReferralsTab() {
// Clipboard + Share funksiyasi // Clipboard + Share funksiyasi
const handleCopyAndShare = async (code: string) => { const handleCopyAndShare = async (code: string) => {
const referralLink = `https://t.me/infotargetbot/join?startapp=${code}`; const referralLink = `${code}`;
// Clipboard-ga nusxa olish // Clipboard-ga nusxa olish
await Clipboard.setStringAsync(referralLink); await Clipboard.setStringAsync(referralLink);
@@ -80,11 +80,10 @@ export function ReferralsTab() {
title: t('Referal linkni ulashish'), title: t('Referal linkni ulashish'),
}); });
} catch (err) { } catch (err) {
console.log('Share error:', err);
} }
if (Platform.OS === 'android') { if (Platform.OS === 'android') {
ToastAndroid.show(t('Refferal kopiya qilindi'), ToastAndroid.SHORT); Toast.success(t('Refferal kopiya qilindi'));
} }
}; };
@@ -153,11 +152,22 @@ export function ReferralsTab() {
</View> </View>
</View> </View>
)} )}
ListEmptyComponent={ ListEmptyComponent={() => (
<Text style={{ textAlign: 'center', color: theme.subText }}> <View style={styles.emptyContainer}>
{t('Refferallar topilmadi')} <View
style={[
styles.emptyIconWrapper,
{ backgroundColor: theme.cardBg }
]}
>
<Gift size={40} color={theme.primary} />
</View>
<Text style={[styles.emptyTitle, { color: theme.text }]}>
{t("Refferallar mavjud emas")}
</Text> </Text>
} </View>
)}
/> />
</View> </View>
); );
@@ -175,7 +185,7 @@ const styles = StyleSheet.create({
}, },
headerTitle: { fontSize: 18, fontWeight: '700' }, headerTitle: { fontSize: 18, fontWeight: '700' },
list: { padding: 16, gap: 12, paddingBottom: 30 }, list: { padding: 16, gap: 12, paddingBottom: 30, flexGrow: 1 },
card: { card: {
borderRadius: 16, borderRadius: 16,
@@ -217,4 +227,46 @@ const styles = StyleSheet.create({
amount: { amount: {
fontWeight: '700', fontWeight: '700',
}, },
emptyContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 30,
},
emptyIconWrapper: {
width: 80,
height: 80,
borderRadius: 40,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 20,
elevation: 4,
},
emptyTitle: {
fontSize: 18,
fontWeight: '700',
marginBottom: 8,
textAlign: 'center',
},
emptyDesc: {
fontSize: 14,
textAlign: 'center',
marginBottom: 20,
lineHeight: 20,
},
emptyButton: {
paddingHorizontal: 24,
paddingVertical: 12,
borderRadius: 12,
},
emptyButtonText: {
color: '#fff',
fontWeight: '600',
fontSize: 15,
},
}); });