Compare commits

...

12 Commits

Author SHA1 Message Date
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
azizziy
bdc205b538 fix: webview services 2026-03-02 09:49:02 +05:00
azizziy
a0e5582fc7 Merge branch 'main' of https://gitea.felixits.uz/Samandar/info-target-mobile 2026-03-02 09:25:52 +05:00
azizziy
dc7e39a7e0 package json 2026-03-02 09:25:47 +05:00
Samandar Turgunboyev
ad0d0d796c reffreal input 2026-02-26 15:44:21 +05:00
Samandar Turgunboyev
7aa130bdd6 update version 2026-02-25 11:10:37 +05:00
Samandar Turgunboyev
a54af819c5 registre page update 2026-02-25 10:27:27 +05:00
Samandar Turgunboyev
8edd45d1ad update register page ui and api 2026-02-24 11:28:06 +05:00
62 changed files with 15504 additions and 855 deletions

1
.npmrc
View File

@@ -1 +1,2 @@
legacy-peer-deps=true
node-linker=hoisted

View File

@@ -28,7 +28,7 @@ export const API_URLS = {
User_Update: 'auth/user-update/',
Employee_List: 'api/employee/',
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_Refferals: 'api/referral/',
Goverment_Service: '/api/goverment-service/',
@@ -36,4 +36,6 @@ export const API_URLS = {
Notification_List: '/api/notifications/',
Notification_Ready: (id: number) => `/api/notifications/${id}/read/`,
Notification_Mark_All_Read: '/api/notifications/read-all/',
Info: "/auth/get-person/",
Get_Director_Info: "/auth/get-director/"
};

View File

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

View File

@@ -1,6 +1,14 @@
// app/(auth)/_layout.tsx
import { RegisterProvider } from '@/screens/auth/register/lib/useRegisterStore';
import { Slot } from 'expo-router';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
export default function AuthLayout() {
return <Slot />;
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<RegisterProvider>
<Slot />
</RegisterProvider>
</GestureHandlerRootView>
)
}

View File

@@ -0,0 +1,15 @@
import RegisterFormScreen from '@/screens/auth/register/RegisterForm';
import React from 'react';
import { StyleSheet, View } from 'react-native';
export default function Index() {
return (
<View style={styles.safeArea}>
<RegisterFormScreen />
</View>
);
}
const styles = StyleSheet.create({
safeArea: { flex: 1, backgroundColor: '#0f172a' },
});

View File

@@ -1,21 +1,247 @@
import RegisterScreen from '@/screens/auth/register/RegisterScreen';
import AuthHeader from '@/components/ui/AuthHeader';
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 { ScrollView, StyleSheet, View } from 'react-native';
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 (
<View style={styles.safeArea}>
<ScrollView
contentContainerStyle={{ flexGrow: 1 }}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
<Animated.View style={{ transform: [{ scale }], marginBottom: 14 }}>
<TouchableOpacity
style={styles.card}
onPress={onPress}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
activeOpacity={1}
testID={`person-type-${item.key}`}
>
<RegisterScreen />
</ScrollView>
<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({
safeArea: { flex: 1, backgroundColor: '#0f172a' },
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

@@ -28,10 +28,15 @@ interface Category {
export default function CategorySelectScreen() {
const router = useRouter();
const { t } = useTranslation();
const { phone, stir, person_type } = useLocalSearchParams<{
const { phone, stir, person_type, director_full_name, referal, first_name, last_name, middle_name } = useLocalSearchParams<{
phone: string;
stir: string;
person_type: 'band' | 'ytt';
referal: string;
director_full_name: string;
first_name: string;
last_name: string;
middle_name: string;
}>();
const [selected, setSelected] = useState<number | null>(null);
@@ -63,6 +68,10 @@ export default function CategorySelectScreen() {
stir: string;
person_type: string;
activate_types: number[];
director_full_name: string;
referal: string;
first_name: string;
last_name: string;
}) => auth_api.register(body),
onSuccess: async () => {
router.replace('/(auth)/register-confirm');
@@ -94,6 +103,8 @@ export default function CategorySelectScreen() {
setSelected(null);
};
const full_name = first_name.length > 0 ? first_name + ' ' + last_name + ' ' + middle_name : director_full_name;
return (
<View style={styles.safeArea}>
<AuthHeader />
@@ -138,6 +149,10 @@ export default function CategorySelectScreen() {
person_type,
phone: `998${phone}`,
stir,
referal: referal,
director_full_name: director_full_name,
first_name: full_name,
last_name: last_name,
});
}}
>

View File

@@ -5,9 +5,9 @@ import { useHomeStore } from '@/screens/home/lib/hook';
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
import { router, Tabs } from 'expo-router';
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 { 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';
export default function TabsLayout() {
@@ -15,6 +15,20 @@ export default function TabsLayout() {
const { t } = useTranslation();
const { setShowFilter, setStep } = useHomeStore();
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();
};
}, []);
console.log(keyboard);
useEffect(() => {
Animated.loop(
@@ -52,9 +66,10 @@ export default function TabsLayout() {
headerShadowVisible: false,
tabBarStyle: {
position: 'absolute',
display: keyboard ? "none" : "flex",
left: 16,
right: 16,
bottom: 4,
bottom: 8,
height: 70,
paddingTop: 8,
paddingBottom: 12,

View File

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

View File

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

View File

@@ -14,10 +14,10 @@ import {
ScrollView,
StyleSheet,
Text,
ToastAndroid,
TouchableOpacity,
View,
} from 'react-native';
import { Toast } from 'toastify-react-native';
export default function PersonalInfoScreen() {
const router = useRouter();
@@ -76,7 +76,7 @@ export default function PersonalInfoScreen() {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['get_me'] });
router.push('/profile/personal-info');
ToastAndroid.show(t("Ma'lumotlar yangilandi"), ToastAndroid.TOP);
Toast.success(t("Ma'lumotlar yangilandi"));
},
onError: () => {
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 {
ActivityIndicator,
Alert,
Image,
Pressable,
ScrollView,
StyleSheet,
Text,
TextInput,
ToastAndroid,
TouchableOpacity,
View,
View
} from 'react-native';
import { Toast } from 'toastify-react-native';
export default function PersonalInfoScreen() {
const router = useRouter();
@@ -71,10 +70,10 @@ export default function PersonalInfoScreen() {
queryClient.invalidateQueries({ queryKey: ['get_me'] });
setIsEditing(false);
setShowCategories(false);
ToastAndroid.show(t("Ma'lumotlar yangilandi"), ToastAndroid.TOP);
Toast.success(t("Ma'lumotlar yangilandi"));
},
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 { router } from 'expo-router';
import { createContext, useContext, useEffect, useState } from 'react';
type AuthContextType = {
@@ -32,6 +33,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
await AsyncStorage.removeItem('access_token');
await AsyncStorage.removeItem('refresh_token');
setIsAuthenticated(false);
router.replace('/(auth)');
};
return (

View File

@@ -1,8 +1,9 @@
import { useTheme } from '@/components/ThemeContext';
import { products_api } from '@/screens/home/lib/api';
import { ProductResponse } from '@/screens/home/lib/types';
import { user_api } from '@/screens/profile/lib/api';
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 { Info, Package, PlayCircle } from 'lucide-react-native';
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 {
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) => {
setSelectedProduct(product);
setCurrentImageIndex(0);
@@ -181,12 +193,29 @@ export default function ProductList({ query }: Props) {
style={styles.sheetContent}
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}>
<FlatList
nestedScrollEnabled={true}
data={selectedProduct.files || []}
data={detail.files || []}
renderItem={renderCarouselItem}
keyExtractor={(item) => item.id.toString()}
horizontal
@@ -197,9 +226,9 @@ export default function ProductList({ query }: Props) {
setCurrentImageIndex(index);
}}
/>
{selectedProduct.files.length > 1 && (
{detail.files.length > 1 && (
<View style={styles.pagination}>
{selectedProduct.files.map((_, i) => (
{detail.files.map((_, i) => (
<View
key={i}
style={[
@@ -214,10 +243,10 @@ export default function ProductList({ query }: Props) {
<View style={styles.sheetHeader}>
<Text style={[styles.sheetTitle, isDark ? styles.darkText : styles.lightText]}>
{selectedProduct.title}
{detail.title}
</Text>
<View style={styles.sheetCompanyBadge}>
<Text style={styles.sheetCompanyText}>{selectedProduct.company}</Text>
<Text style={styles.sheetCompanyText}>{detail.company}</Text>
</View>
</View>
@@ -230,10 +259,8 @@ export default function ProductList({ query }: Props) {
{t("Batafsil ma'lumot")}
</Text>
</View>
<Text
style={[styles.sheetDescription, isDark ? styles.darkText : styles.lightText]}
>
{selectedProduct.description || "Ma'lumot mavjud emas."}
<Text style={[styles.sheetDescription, isDark ? styles.darkText : styles.lightText]}>
{detail.description || "Ma'lumot mavjud emas."}
</Text>
</View>
</>
@@ -245,7 +272,6 @@ export default function ProductList({ query }: Props) {
}
const styles = StyleSheet.create({
listContainer: { gap: 0, paddingBottom: 20 },
card: {
borderRadius: 16,
overflow: 'hidden',

View File

@@ -139,7 +139,7 @@
"Keyingi": "Next",
"Xizmat sarlavhasi": "Service title",
"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)",
"Tilni tanlang": "Select language",
"Rejimni tanlang": "Select mode",
@@ -208,5 +208,24 @@
"Bekor qilish": "Cancel",
"Barchasi o'qildi": "Mark All as Read",
"Hisobni o'chirish": "Delete account",
"Rostdan ham hisobingizni ochirmoqchimisiz?": "Are you sure you want to delete your account?"
"Rostdan ham hisobingizni ochirmoqchimisiz?": "Are you sure you want to delete your account?",
"Referal": "Referal",
"Faoliyat turingizni tanlang": "Select your type of activity",
"YATT": "Sole Proprietor",
"Yakka tartibdagi tadbirkor": "Individual entrepreneur",
"O'zini o'zi band qilgan": "Self-employed",
"O'z faoliyatini mustaqil yurituvchi": "Independently operating individual",
"Yuridik shaxs": "Legal entity",
"Tashkilot yoki korxona": "Organization or company",
"JSHSHR va passport ma'lumotlarini kiriting": "Enter JSHSHR and passport details",
"INN raqamini kiriting": "Enter INN number",
"JSHSHR": "JSHSHR",
"JSHSHR kiriting (14 raqam)": "Enter JSHSHR (14 digits)",
"Passport seriya va raqami": "Passport series and number",
"INN": "INN",
"INN kiriting (9 raqam)": "Enter INN (9 digits)",
"Referal kodi": "Referral code",
"Direktor JSHSHR": "Director JSHSHR",
"Direktor JSHSHR (14 raqam)": "Director JSHSHR (number 14)",
"Hozircha bildirishnomalar yo'q": "No notifications yet"
}

View File

@@ -139,7 +139,7 @@
"Keyingi": "Далее",
"Xizmat sarlavhasi": "Заголовок услуги",
"Yangilashda xato yuz berdi": "Произошла ошибка при обновлении",
"Xizmatni tahrirlash (1/2)": "Редактирование услуги (1/2)",
"Xizmatni tahrirlash": "Редактирование услуги",
"Xizmatni tahrirlash (2/2)": "Редактирование услуги (2/2)",
"Tilni tanlang": "Выберите язык",
"Rejimni tanlang": "Выберите режим",
@@ -207,5 +207,24 @@
"Bekor qilish": "Отменить",
"Barchasi o'qildi": "Отметить все как прочитанное",
"Hisobni o'chirish": "Удалить аккаунт",
"Rostdan ham hisobingizni o'chirmoqchimisiz?": "Вы уверены, что хотите удалить свой аккаунт?"
"Rostdan ham hisobingizni o'chirmoqchimisiz?": "Вы уверены, что хотите удалить свой аккаунт?",
"Referal": "Реферал",
"Faoliyat turingizni tanlang": "Выберите тип деятельности",
"YATT": "ИП",
"Yakka tartibdagi tadbirkor": "Индивидуальный предприниматель",
"O'zini o'zi band qilgan": "Самозанятый",
"O'z faoliyatini mustaqil yurituvchi": "Лицо, ведущее самостоятельную деятельность",
"Yuridik shaxs": "Юридическое лицо",
"Tashkilot yoki korxona": "Организация или компания",
"JSHSHR va passport ma'lumotlarini kiriting": "Введите ПИНФЛ и паспортные данные",
"INN raqamini kiriting": "Введите номер ИНН",
"JSHSHR": "ПИНФЛ",
"JSHSHR kiriting (14 raqam)": "Введите ПИНФЛ (14 цифр)",
"Passport seriya va raqami": "Серия и номер паспорта",
"INN": "ИНН",
"INN kiriting (9 raqam)": "Введите ИНН (9 цифр)",
"Referal kodi": "Реферальный код",
"Direktor JSHSHR": "Директор ПИНФЛ",
"Direktor JSHSHR kiriting (14 raqam)": "Введите ПИНФЛ директора (14 цифр)",
"Hozircha bildirishnomalar yo'q": "Пока нет уведомлений"
}

View File

@@ -139,7 +139,7 @@
"Yangi xizmat (2/2)": "Yangi xizmat (2/2)",
"Keyingi": "Keyingi",
"Xizmat sarlavhasi": "Xizmat sarlavhasi",
"Xizmatni tahrirlash (1/2)": "Xizmatni tahrirlash (1/2)",
"Xizmatni tahrirlash": "Xizmatni tahrirlash",
"Xizmatni tahrirlash (2/2)": "Xizmatni tahrirlash (2/2)",
"Tilni tanlang": "Tilni tanlang",
"Rejimni tanlang": "Rejimni tanlang",
@@ -177,6 +177,7 @@
"Faqat tasdiqlangan profillarga ishonch bilan murojaat qiling.": "Faqat tasdiqlangan profillarga ishonch bilan murojaat qiling.",
"Qo'llanma video": "Qo'llanma video",
"Bildirishnomalar": "Bildirishnomalar",
"Hozircha bildirishnomalar yo'q": "Hozircha bildirishnomalar yo'q",
"Bildirishnomalarni yuklashda muammo bo'ldi": "Bildirishnomalarni yuklashda muammo bo'ldi",
"Hozir": "Hozir",
"daqiqa oldin": "daqiqa oldin",
@@ -207,5 +208,23 @@
"Bekor qilish": "Bekor qilish",
"Barchasi o'qildi": "Barchasi o'qildi",
"Hisobni o'chirish": "Hisobni o'chirish",
"Rostdan ham hisobingizni o'chirmoqchimisiz?": "Rostdan ham hisobingizni o'chirmoqchimisiz?"
"Rostdan ham hisobingizni o'chirmoqchimisiz?": "Rostdan ham hisobingizni o'chirmoqchimisiz?",
"Referal": "Referal",
"Faoliyat turingizni tanlang": "Faoliyat turingizni tanlang",
"YATT": "YATT",
"Yakka tartibdagi tadbirkor": "Yakka tartibdagi tadbirkor",
"O'zini o'zi band qilgan": "O'zini o'zi band qilgan",
"O'z faoliyatini mustaqil yurituvchi": "O'z faoliyatini mustaqil yurituvchi",
"Yuridik shaxs": "Yuridik shaxs",
"Tashkilot yoki korxona": "Tashkilot yoki korxona",
"JSHSHR va passport ma'lumotlarini kiriting": "JSHSHR va passport ma'lumotlarini kiriting",
"INN raqamini kiriting": "INN raqamini kiriting",
"JSHSHR": "JSHSHR",
"JSHSHR kiriting (14 raqam)": "JSHSHR kiriting (14 raqam)",
"Passport seriya va raqami": "Passport seriya va raqami",
"INN": "INN",
"INN kiriting (9 raqam)": "INN kiriting (9 raqam)",
"Referal kodi": "Referal kodi",
"Direktor JSHSHR": "Direktor JSHSHR",
"Direktor JSHSHR kiriting (14 raqam)": "Direktor JSHSHR kiriting (14 raqam)"
}

81
package-lock.json generated
View File

@@ -71,6 +71,7 @@
"react-native-worklets": "^0.5.1",
"react-stately": "^3.39.0",
"tailwind-variants": "^0.1.20",
"toastify-react-native": "^7.2.3",
"zustand": "^5.0.10"
},
"devDependencies": {
@@ -15396,6 +15397,73 @@
"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": {
"version": "0.21.2",
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz",
@@ -17202,6 +17270,19 @@
"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": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",

View File

@@ -9,7 +9,8 @@
"ios": "expo run:ios",
"web": "expo start --web",
"build": "expo prebuild && cd android && gradlew.bat assembleDebug",
"lint": "expo lint"
"lint": "expo lint",
"ios:init": "expo prebuild -p ios"
},
"dependencies": {
"@expo/html-elements": "^0.10.1",
@@ -75,6 +76,7 @@
"react-native-worklets": "^0.5.1",
"react-stately": "^3.39.0",
"tailwind-variants": "^0.1.20",
"toastify-react-native": "^7.2.3",
"zustand": "^5.0.10"
},
"devDependencies": {

12876
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -101,14 +101,14 @@ export default function DashboardScreen() {
// Announcement videos
const videos = {
uz: require('@/assets/announcements-video/video_uz.webm'),
ru: require('@/assets/announcements-video/video_ru.webm'),
en: require('@/assets/announcements-video/video_en.webm'),
uz: require('@/assets/announcements-video/video_uz.mp4'),
ru: require('@/assets/announcements-video/video_ru.mp4'),
en: require('@/assets/announcements-video/video_en.mp4'),
};
// Government videos: faqat RU mavjud
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

View File

@@ -16,12 +16,11 @@ import {
Platform,
StyleSheet,
Text,
ToastAndroid,
TouchableOpacity,
View,
View
} from 'react-native';
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 useTokenStore from '../login/lib/hook';
import ConfirmForm from './ConfirmForm';
@@ -98,11 +97,11 @@ const ConfirmScreen = () => {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
setResendTimer(60);
if (Platform.OS === 'android') {
ToastAndroid.show(t('Kod qayta yuborildi'), ToastAndroid.SHORT);
Toast.info(t('Kod qayta yuborildi'));
}
},
onError: () => {
Alert.alert(t('Xatolik yuz berdi'), t('Kodni qayta yuborishda xatolik yuz berdi'));
Toast.error(t('Kodni qayta yuborishda xatolik yuz berdi'));
},
});
@@ -130,72 +129,71 @@ const ConfirmScreen = () => {
<View style={styles.decorCircle1} />
<View style={styles.decorCircle2} />
<AuthHeader />
<SafeAreaView style={{ flex: 1 }}>
<KeyboardAwareScrollView
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
>
<View style={styles.header}>
<View style={styles.iconContainer}>
<LinearGradient colors={['#3b82f6', '#2563eb']} style={styles.iconGradient}>
<ShieldCheck size={32} color="#ffffff" strokeWidth={2.2} />
</LinearGradient>
</View>
<KeyboardAwareScrollView
contentContainerStyle={styles.scrollContent}
enableOnAndroid
extraScrollHeight={120}
keyboardShouldPersistTaps="handled"
>
<View style={styles.header}>
<View style={styles.iconContainer}>
<LinearGradient colors={['#3b82f6', '#2563eb']} style={styles.iconGradient}>
<ShieldCheck size={32} color="#ffffff" strokeWidth={2.2} />
</LinearGradient>
</View>
<Text style={styles.title}>{t('Kodni tasdiqlash')}</Text>
<Text style={styles.subtitle}>
{t("Tasdiqlash kodi sizning Telegram botingizga yuboriladi. Botni ko'rish")}
</Text>
<Text style={styles.title}>{t('Kodni tasdiqlash')}</Text>
<Text style={styles.subtitle}>
{t("Tasdiqlash kodi sizning Telegram botingizga yuboriladi. Botni ko'rish")}
</Text>
<View style={styles.phoneBadge}>
<Text style={styles.phoneText}>+{phoneOTP}</Text>
</View>
<View style={styles.phoneBadge}>
<Text style={styles.phoneText}>+{phoneOTP}</Text>
</View>
{/* Telegram Button */}
<TouchableOpacity
style={styles.telegramBanner}
onPress={openBotLink}
activeOpacity={0.8}
{/* Telegram Button */}
<TouchableOpacity
style={styles.telegramBanner}
onPress={openBotLink}
activeOpacity={0.8}
>
<LinearGradient
colors={['#0088cc', '#00a2ed']}
style={styles.telegramGradient}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
>
<LinearGradient
colors={['#0088cc', '#00a2ed']}
style={styles.telegramGradient}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
>
<View style={styles.botIconCircle}>
<MessageCircle size={20} color="#0088cc" fill="#fff" />
</View>
<View style={{ flex: 1 }}>
<Text style={styles.telegramTitle}>{t('Botni ochish')}</Text>
<Text style={styles.telegramSub}>
{t('Telegram botni ochish uchun tugmani bosing va kodni oling')}
</Text>
</View>
<ArrowLeft size={20} color="#fff" style={{ transform: [{ rotate: '180deg' }] }} />
</LinearGradient>
</TouchableOpacity>
</View>
<View style={styles.botIconCircle}>
<MessageCircle size={20} color="#0088cc" fill="#fff" />
</View>
<View style={{ flex: 1 }}>
<Text style={styles.telegramTitle}>{t('Botni ochish')}</Text>
<Text style={styles.telegramSub}>
{t('Telegram botni ochish uchun tugmani bosing va kodni oling')}
</Text>
</View>
<ArrowLeft size={20} color="#fff" style={{ transform: [{ rotate: '180deg' }] }} />
</LinearGradient>
</TouchableOpacity>
</View>
<View style={styles.card}>
<ConfirmForm
onSubmit={(otp) => mutate({ code: otp, phone: phoneOTP || '' })}
isLoading={isPending}
error={error}
onResendPress={() => resendMutation.mutate({ phone: phoneOTP || '' })}
resendTimer={resendTimer}
/>
</View>
<View style={styles.card}>
<ConfirmForm
onSubmit={(otp) => mutate({ code: otp, phone: phoneOTP || '' })}
isLoading={isPending}
error={error}
onResendPress={() => resendMutation.mutate({ phone: phoneOTP || '' })}
resendTimer={resendTimer}
/>
</View>
{/* <View style={styles.infoBox}>
{/* <View style={styles.infoBox}>
<Text style={styles.infoText}>
<Text style={{ fontWeight: '700' }}>Eslatma:</Text> Kod SMS orqali kelmaydi. Agar
botni ishga tushirmagan bo'lsangiz, yuqoridagi tugmani bosing.
</Text>
</View> */}
</KeyboardAwareScrollView>
</SafeAreaView>
</KeyboardAwareScrollView>
</View>
);
};

View File

@@ -1,6 +1,6 @@
import httpClient from '@/api/httpClient';
import { API_URLS } from '@/api/URLs';
import axios, { AxiosResponse } from 'axios';
import { AxiosResponse } from 'axios';
interface ConfirmBody {
status: boolean;
@@ -13,6 +13,310 @@ interface ConfirmBody {
};
}
export interface CompanyInfo {
id: number;
inn: string;
registration_authority: string;
registration_date: string;
registration_number: string;
name: string;
short_name: string;
opf_code: string;
opf_name: string;
oked_code: string;
vat_number: string;
oked_name: string;
area: string;
region: string;
soogu_code: string;
soogu_name: string;
small_businesses: string;
activity_state: number;
statutory_fund: string;
activity_state_detail: ActivityStateDetail;
business_type_detail: BusinessTypeDetail;
director: string;
email: string;
village_code: string;
email_status: number;
phones: string[];
soato_code: string;
soato_name: 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 {
id: number;
external_id: number;
name: string;
name_uz: string;
name_en: string;
}
export interface DishonestExecutor {
is_dishonest_executor: number;
delete_date: string | null;
}
export interface VillageDetail {
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,
data: {
entity: {
name: {
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
},
inn: {
rows: [],
total: 0
},
director: {
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,
pinfl: string,
entrepreneur: string,
email: string,
phone: string,
registration_date: string
}[],
total: number
},
trademark: {
rows: [],
total: number
}
}
}
export const auth_api = {
async login(body: { phone: string }) {
const res = await httpClient.post(API_URLS.LOGIN, body);
@@ -28,8 +332,13 @@ export const auth_api = {
return res;
},
async get_info(inn: string) {
const res = await axios.get(`https://devapi.goodsign.biz/v1/profile/${inn}`);
async get_info(body: { value: string, type: string, passport_series?: string, passport_number?: string }): Promise<AxiosResponse<GetInfoResponse>> {
const res = await httpClient.post(API_URLS.Info, body);
return res;
},
async get_director_info(body: { value: string }): Promise<AxiosResponse<GetDirectorInfoResponse>> {
const res = await httpClient.post(API_URLS.Get_Director_Info, body);
return res;
},
@@ -37,7 +346,11 @@ export const auth_api = {
phone: string;
stir: string;
person_type: string;
referal: string;
activate_types: number[];
director_full_name: string;
first_name: string;
last_name: string;
}) {
const res = await httpClient.post(API_URLS.Register, body);
return res;

View File

@@ -56,7 +56,7 @@ export default function LoginScreen() {
<TouchableOpacity
style={styles.registerButton}
onPress={() => router.push('/register')}
onPress={() => router.push('/(auth)/register')}
activeOpacity={0.8}
>
<LinearGradient

View File

@@ -16,12 +16,12 @@ import {
Platform,
StyleSheet,
Text,
ToastAndroid,
TouchableOpacity,
View,
View
} from 'react-native';
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 useTokenStore from '../login/lib/hook';
import ConfirmForm from './ConfirmForm';
@@ -93,7 +93,7 @@ const RegisterConfirmScreen = () => {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
setResendTimer(60);
if (Platform.OS === 'android') {
ToastAndroid.show(t('Kod qayta yuborildi'), ToastAndroid.SHORT);
Toast.info(t('Kod qayta yuborildi'));
}
},
onError: () => {

View File

@@ -0,0 +1,48 @@
import { useRouter } from 'expo-router';
import { ChevronLeft } from 'lucide-react-native';
import React from 'react';
import { StyleSheet, TouchableOpacity, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
export default function AuthHeader() {
const router = useRouter();
const insets = useSafeAreaInsets();
return (
<View style={[styles.container, { paddingTop: insets.top + 8 }]}>
<TouchableOpacity
style={styles.backButton}
onPress={() => {
if (router.canGoBack()) {
router.back();
}
}}
testID="auth-header-back"
>
<ChevronLeft size={22} color="#94a3b8" />
</TouchableOpacity>
<View style={{ width: 44 }} />
</View>
);
}
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 24,
paddingBottom: 8,
zIndex: 1000,
},
backButton: {
width: 44,
height: 44,
backgroundColor: 'rgba(255, 255, 255, 0.1)',
borderRadius: 12,
alignItems: 'center',
justifyContent: 'center',
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.15)',
},
});

View File

@@ -9,12 +9,18 @@ import { ScrollView, StyleSheet, Text, TouchableOpacity } from 'react-native';
export default function CategorySelectScreen() {
const router = useRouter();
const { phone, stir, person_type } = useLocalSearchParams<{
const { phone, stir, person_type, director_full_name, referal, first_name, last_name } = useLocalSearchParams<{
phone: string;
stir: string;
person_type: 'band' | 'ytt';
referal: string;
director_full_name: string;
first_name: string;
last_name: string;
}>();
console.log(referal);
const [selected, setSelected] = React.useState<number | null>(null);
const { data } = useQuery({
@@ -28,6 +34,10 @@ export default function CategorySelectScreen() {
stir: string;
person_type: string;
activate_types: number[];
referal: string;
director_full_name: string;
first_name: string;
last_name: string;
}) => auth_api.register(body),
onSuccess: () => router.replace('/'),
});
@@ -52,12 +62,16 @@ export default function CategorySelectScreen() {
disabled={!selected || isPending}
style={[styles.bottom, (!selected || isPending) && { opacity: 0.5 }]}
onPress={() => {
if (phone && stir && person_type && selected) {
if (phone && stir && person_type && selected && referal && director_full_name) {
mutate({
activate_types: [selected],
person_type: person_type,
phone: `998${phone}`,
stir: stir,
referal: String(referal),
director_full_name: String(director_full_name),
first_name: String(first_name),
last_name: String(last_name),
});
}
}}

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +1,112 @@
import AuthHeader from '@/components/ui/AuthHeader';
import { LinearGradient } from 'expo-linear-gradient';
import { useRouter } from 'expo-router';
import { UserPlus } from 'lucide-react-native';
import { Building2, ChevronRight, ShieldCheck, User } from 'lucide-react-native';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import {
Animated,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import RegisterForm from './RegisterForm';
import { PersonType, useRegister } from './lib/useRegisterStore';
export default function RegisterScreen() {
const PERSON_TYPES: {
key: PersonType;
label: string;
description: string;
icon: React.ReactNode;
gradient: [string, string];
}[] = [
{
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'],
},
{
key: 'legal_entity',
label: 'Yuridik shaxs',
description: "Tashkilot yoki korxona",
icon: <Building2 size={28} color="#fff" />,
gradient: ['#f59e0b', '#d97706'],
},
];
function TypeCard({
item,
index,
onPress,
}: {
item: (typeof PERSON_TYPES)[number];
index: number;
onPress: () => void;
}) {
const scale = React.useRef(new Animated.Value(1)).current;
const handlePressIn = () => {
Animated.spring(scale, {
toValue: 0.96,
useNativeDriver: true,
}).start();
};
const handlePressOut = () => {
Animated.spring(scale, {
toValue: 1,
friction: 4,
useNativeDriver: true,
}).start();
};
return (
<Animated.View style={{ transform: [{ scale }] }}>
<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}>{item.label}</Text>
<Text style={styles.cardDescription}>{item.description}</Text>
</View>
<ChevronRight size={20} color="#94a3b8" />
</TouchableOpacity>
</Animated.View>
);
}
export default function PersonTypeScreen() {
const router = useRouter();
const { t } = useTranslation();
const { setPersonType } = useRegister();
const handleSelect = (type: PersonType) => {
setPersonType(type);
router.push('/(auth)/register-form');
};
return (
<View style={styles.container}>
{/* Background Decorations */}
<LinearGradient
colors={['#0f172a', '#1e293b', '#334155']}
start={{ x: 0, y: 0 }}
@@ -24,119 +116,54 @@ export default function RegisterScreen() {
<View style={styles.decorCircle1} />
<View style={styles.decorCircle2} />
<AuthHeader />
<SafeAreaView style={{ flex: 1 }}>
<ScrollView
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
{/* Header Section */}
<View style={styles.header}>
<View style={styles.iconContainer}>
<LinearGradient
colors={['#10b981', '#059669']}
style={styles.iconGradient}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
>
<UserPlus size={32} color="#ffffff" />
</LinearGradient>
</View>
<Text style={styles.title}>{t("Ro'yxatdan o'tish")}</Text>
<Text style={styles.subtitle}>
{t('Tizimdan foydalanish uchun STIR raqami yoki JSHSHR kiritishingiz kerak.')}
<SafeAreaView style={styles.safeArea}>
<View style={styles.header}>
<Text style={styles.title}>Ro'yxatdan o'tish</Text>
<Text style={styles.subtitle}>
Faoliyat turingizni tanlang
</Text>
</View>
<View style={styles.cardList}>
{PERSON_TYPES.map((item, index) => (
<TypeCard
key={item.key}
item={item}
index={index}
onPress={() => handleSelect(item.key)}
/>
))}
</View>
<View style={styles.footer}>
<TouchableOpacity onPress={() => router.back()} testID="go-to-login">
<Text style={styles.footerText}>
Hisobingiz bormi? <Text style={styles.footerLink}>Kirish</Text>
</Text>
</View>
{/* Form Card */}
<View style={styles.card}>
<RegisterForm />
</View>
{/* Footer */}
<View style={styles.footer}>
<TouchableOpacity onPress={() => router.push('/')}>
<Text style={styles.footerText}>
{t('Hisobingiz bormi?')} <Text style={styles.footerLink}>{t('Kirish')}</Text>
</Text>
</TouchableOpacity>
</View>
</ScrollView>
</TouchableOpacity>
</View>
</SafeAreaView>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#0f172a' },
// Header Navigatsiya qismi (LoginScreen kabi)
languageHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
container: {
flex: 1,
backgroundColor: '#0f172a',
},
safeArea: {
flex: 1,
paddingHorizontal: 24,
paddingVertical: 12,
zIndex: 1000,
},
backButton: {
width: 44,
height: 44,
backgroundColor: 'rgba(255, 255, 255, 0.1)',
borderRadius: 12,
alignItems: 'center',
justifyContent: 'center',
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.15)',
},
languageButton: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
backgroundColor: 'rgba(255, 255, 255, 0.1)',
paddingHorizontal: 16,
paddingVertical: 10,
borderRadius: 12,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.15)',
},
languageText: {
fontSize: 14,
fontWeight: '600',
color: '#94a3b8',
},
// Scroll va Forma joylashuvi
scrollContent: {
flexGrow: 1,
paddingHorizontal: 24,
paddingBottom: 40,
paddingTop: 10,
justifyContent: 'center',
},
header: {
alignItems: 'center',
marginBottom: 32,
},
iconContainer: {
marginBottom: 20,
},
iconGradient: {
width: 72,
height: 72,
borderRadius: 22,
alignItems: 'center',
justifyContent: 'center',
shadowColor: '#10b981',
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.3,
shadowRadius: 12,
elevation: 8,
marginTop: 40,
marginBottom: 36,
},
title: {
fontSize: 28,
fontWeight: '800',
fontWeight: '800' as const,
color: '#ffffff',
marginBottom: 10,
letterSpacing: 0.5,
@@ -146,61 +173,54 @@ const styles = StyleSheet.create({
color: '#94a3b8',
textAlign: 'center',
lineHeight: 22,
paddingHorizontal: 10,
},
cardList: {
gap: 14,
},
card: {
backgroundColor: '#ffffff',
borderRadius: 28,
padding: 24,
shadowColor: '#000',
shadowOffset: { width: 0, height: 10 },
shadowOpacity: 0.3,
shadowRadius: 20,
elevation: 10,
},
// Dropdown (LoginScreen bilan bir xil)
dropdown: {
position: 'absolute',
top: 55,
right: 0,
backgroundColor: '#ffffff',
borderRadius: 16,
padding: 8,
minWidth: 180,
shadowColor: '#000',
shadowOffset: { width: 0, height: 10 },
shadowOpacity: 0.2,
shadowRadius: 20,
elevation: 15,
borderWidth: 1,
borderColor: '#f1f5f9',
},
dropdownOption: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 12,
paddingHorizontal: 16,
borderRadius: 12,
marginBottom: 4,
backgroundColor: 'rgba(255, 255, 255, 0.06)',
borderRadius: 20,
padding: 18,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.1)',
gap: 16,
},
dropdownOptionActive: { backgroundColor: '#eff6ff' },
dropdownOptionText: { fontSize: 14, fontWeight: '600', color: '#475569' },
dropdownOptionTextActive: { color: '#3b82f6' },
checkmark: {
width: 22,
height: 22,
borderRadius: 11,
backgroundColor: '#3b82f6',
cardIcon: {
width: 56,
height: 56,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
},
checkmarkText: { color: '#ffffff', fontSize: 12, fontWeight: 'bold' },
footer: { marginTop: 24, alignItems: 'center' },
footerText: { color: '#94a3b8', fontSize: 14 },
footerLink: { color: '#3b82f6', fontWeight: '700' },
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,

View File

@@ -0,0 +1,11 @@
import { create } from "zustand";
type State = {
referal: string;
setReferal: (referal: string) => void;
}
export const useRegisterForm = create<State>((set) => ({
referal: '',
setReferal: (referal) => set({ referal }),
}))

View File

@@ -0,0 +1,93 @@
import createContextHook from '@nkzw/create-context-hook';
import { useState } from 'react';
import { GetDirectorInfoResponse } from '../../login/lib/api';
export type PersonType = 'yatt' | 'band' | 'legal_entity' | null;
interface RegisterState {
personType: PersonType;
setPersonType: (type: PersonType) => void;
phone: string;
setPhone: (phone: string) => void;
referal: string;
setReferal: (referal: string) => void;
jshshr: string;
setJshshr: (jshshr: string) => void;
passportSeries: string;
setPassportSeries: (series: string) => void;
passportNumber: string;
setPassportNumber: (number: string) => void;
inn: string;
setInn: (inn: string) => void;
info: any;
setInfo: (info: any) => void;
reset_full: () => void;
reset: () => void;
directorJshshr: string;
setDirectorJshshr: (directorJshshr: string) => void;
directorInfo: GetDirectorInfoResponse | null;
setDirectorInfo: (directorInfo: GetDirectorInfoResponse | null) => void;
}
export const [RegisterProvider, useRegister] = createContextHook<RegisterState>(() => {
const [personType, setPersonType] = useState<PersonType>(null);
const [phone, setPhone] = useState<string>('');
const [referal, setReferal] = useState<string>('');
const [jshshr, setJshshr] = useState<string>('');
const [passportSeries, setPassportSeries] = useState<string>('');
const [passportNumber, setPassportNumber] = useState<string>('');
const [inn, setInn] = useState<string>('');
const [info, setInfo] = useState<any>(null);
const [directorJshshr, setDirectorJshshr] = useState<string>('');
const [directorInfo, setDirectorInfo] = useState<GetDirectorInfoResponse | null>(null);
const reset_full = () => {
setPersonType(null);
setPhone('');
setReferal('');
setJshshr('');
setPassportSeries('');
setPassportNumber('');
setInn('');
setInfo(null);
setDirectorJshshr('');
setDirectorInfo(null);
};
const reset = () => {
setPhone('');
setReferal('');
setJshshr('');
setPassportSeries('');
setPassportNumber('');
setInn('');
setInfo(null);
setDirectorJshshr('');
setDirectorInfo(null);
};
return {
personType,
setPersonType,
phone,
setPhone,
referal,
setReferal,
jshshr,
setJshshr,
passportSeries,
setPassportSeries,
passportNumber,
setPassportNumber,
inn,
setInn,
reset,
info,
setInfo,
reset_full,
directorJshshr,
setDirectorJshshr,
directorInfo,
setDirectorInfo,
};
});

View File

@@ -7,18 +7,19 @@ import React, { useCallback, useRef, useState } from 'react';
import {
Alert,
Image,
KeyboardAvoidingView,
Keyboard,
Linking,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
TouchableWithoutFeedback,
View
} from 'react-native';
import OneClick from '@/assets/images/one_click.png';
import PAYME from '@/assets/images/Payme_NEW.png';
import { useTranslation } from 'react-i18next';
import { KeyboardAwareScrollView, } from 'react-native-keyboard-aware-scroll-view';
import { price_calculation } from '../lib/api';
import { CreateAdsResponse } from '../lib/types';
import StepFour from './StepFour';
@@ -185,13 +186,16 @@ export default function CreateAdsScreens() {
onSuccess: async (res, variables) => {
if (variables.paymentType === 'payme') {
await Linking.openURL(res.data.url);
router.push('/(dashboard)/announcements');
bottomSheetModalRef.current?.dismiss();
router.push('/profile/my-ads');
} else {
router.push('/(dashboard)/announcements');
bottomSheetModalRef.current?.dismiss();
router.push('/profile/my-ads')
}
},
onError: (err) => {
Alert.alert('Xatolik yuz berdi', err.message);
onError: (err: AxiosError) => {
const errMessage = (err.response?.data as { referral_amount: string }).referral_amount
Alert.alert(t('Xatolik yuz berdi'), errMessage || err.message);
},
});
@@ -206,78 +210,83 @@ export default function CreateAdsScreens() {
};
return (
<KeyboardAvoidingView
behavior="padding"
<KeyboardAwareScrollView
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
style={[styles.safeArea, isDark ? styles.darkBg : styles.lightBg]}
>
<ScrollView contentContainerStyle={[styles.container, { paddingBottom: 90 }]}>
<Image
source={OneClick}
style={{ width: 180, height: 56, marginBottom: 10 }}
resizeMode="contain"
/>
<Text style={[styles.title, isDark ? styles.darkText : styles.lightText]}>
{t("Bir Zumda Jonatish")}
</Text>
{currentStep === 1 && (
<StepOne ref={stepOneRef} formData={formData} updateForm={updateForm} />
)}
{currentStep === 2 && (
<StepTwo ref={stepTwoRef} formData={formData} updateForm={updateForm} />
)}
{currentStep === 3 && (
<StepThree
ref={stepThreeRef}
formData={formData}
updateForm={updateForm}
data={data?.data}
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View
style={[styles.container, { marginBottom: Keyboard.isVisible() ? 10 : 90 }]}
onStartShouldSetResponder={() => {
Keyboard.dismiss();
return false;
}}
>
<Image
source={OneClick}
style={{ width: 180, height: 56, marginBottom: 10 }}
resizeMode="contain"
/>
)}
{currentStep === 4 && <StepFour data={ads} setPayment={setPaymentType} />}
<View style={styles.footer}>
{currentStep > 1 && currentStep !== 4 && (
<Text style={[styles.title, isDark ? styles.darkText : styles.lightText]}>
{t("Bir Zumda Jonatish")}
</Text>
{currentStep === 1 && (
<StepOne ref={stepOneRef} formData={formData} updateForm={updateForm} />
)}
{currentStep === 2 && (
<StepTwo ref={stepTwoRef} formData={formData} updateForm={updateForm} />
)}
{currentStep === 3 && (
<StepThree
ref={stepThreeRef}
formData={formData}
updateForm={updateForm}
data={data?.data}
/>
)}
{currentStep === 4 && <StepFour data={ads} setPayment={setPaymentType} />}
<View style={styles.footer}>
{currentStep > 1 && currentStep !== 4 && (
<TouchableOpacity
style={[styles.back, isDark ? styles.darkBack : styles.lightBack]}
onPress={() => setCurrentStep((s) => s - 1)}
>
<Text style={[styles.btnText, isDark ? styles.darkBtnText : styles.lightBtnText]}>
{t('Orqaga')}
</Text>
</TouchableOpacity>
)}
<TouchableOpacity
style={[styles.back, isDark ? styles.darkBack : styles.lightBack]}
onPress={() => setCurrentStep((s) => s - 1)}
style={styles.next}
disabled={isPending}
onPress={() => {
let isValid = true;
if (currentStep === 1) isValid = stepOneRef.current?.validate() ?? false;
if (currentStep === 2) isValid = stepTwoRef.current?.validate() ?? false;
if (currentStep === 3) isValid = stepThreeRef.current?.validate() ?? false;
if (!isValid) return;
if (currentStep < 3) setCurrentStep((s) => s + 1);
if (currentStep === 3) handleSubmit();
if (currentStep === 4) handlePresentModalPress();
}}
>
<Text style={[styles.btnText, isDark ? styles.darkBtnText : styles.lightBtnText]}>
{t('Orqaga')}
<Text style={styles.btnText}>
{currentStep === 3
? t('Yaratish')
: currentStep === 4
? t("To'lash")
: t('Keyingisi')}
</Text>
</TouchableOpacity>
)}
<TouchableOpacity
style={styles.next}
disabled={isPending}
onPress={() => {
let isValid = true;
if (currentStep === 1) isValid = stepOneRef.current?.validate() ?? false;
if (currentStep === 2) isValid = stepTwoRef.current?.validate() ?? false;
if (currentStep === 3) isValid = stepThreeRef.current?.validate() ?? false;
if (!isValid) return;
if (currentStep < 3) setCurrentStep((s) => s + 1);
if (currentStep === 3) handleSubmit();
if (currentStep === 4) handlePresentModalPress();
}}
>
<Text style={styles.btnText}>
{currentStep === 3
? t('Yaratish')
: currentStep === 4
? t("To'lash")
: t('Keyingisi')}
</Text>
</TouchableOpacity>
</View>
</View>
</ScrollView>
{/* FOOTER */}
{/* PAYMENT BOTTOM SHEET */}
</TouchableWithoutFeedback>
<BottomSheetModal
ref={bottomSheetModalRef}
index={0}
@@ -321,7 +330,7 @@ export default function CreateAdsScreens() {
</View>
</BottomSheetScrollView>
</BottomSheetModal>
</KeyboardAvoidingView>
</KeyboardAwareScrollView>
);
}

View File

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

View File

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

View File

@@ -1,42 +1,57 @@
import Express_diagnistika from '@/assets/images/Express_diagnistika.png';
import { useTheme } from '@/components/ThemeContext';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { Image } from 'expo-image';
import { router } from 'expo-router';
import { ChevronLeft, XIcon } from 'lucide-react-native';
import React, { useRef, useState } from 'react';
import { ActivityIndicator, FlatList, Modal, Text, TouchableOpacity, View } from 'react-native';
import { RefreshControl } from 'react-native-gesture-handler';
import { SafeAreaView } from 'react-native-safe-area-context';
import { WebView } from 'react-native-webview';
import { eservices_api } from '../lib/api';
import Express_diagnistika from "@/assets/images/Express_diagnistika.png";
import { useTheme } from "@/components/ThemeContext";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { Image } from "expo-image";
import { router } from "expo-router";
import * as WebBrowser from "expo-web-browser";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import {
ActivityIndicator,
FlatList,
Text,
TouchableOpacity,
View
} from "react-native";
import { RefreshControl } from "react-native-gesture-handler";
import { Toast } from "toastify-react-native";
import { eservices_api } from "../lib/api";
const dark = {
bg: '#0f172a',
card: '#334155',
border: '#1e293b',
muted: '#334155',
text: '#E5B037',
subText: '#0B0F2C',
bg: "#0f172a",
card: "#334155",
border: "#1e293b",
muted: "#334155",
text: "#E5B037",
subText: "#0B0F2C",
};
export default function EServicesCategoryScreen() {
const { isDark } = useTheme();
const [modalVisible, setModalVisible] = useState(false);
const webviewRef = useRef<WebView>(null);
const [webUrl, setWebUrl] = React.useState<string | null>(null);
const [refreshing, setRefreshing] = useState(false);
const queryClient = useQueryClient();
const { t } = useTranslation();
const { data, isLoading } = useQuery({
queryKey: ['goverment_category'],
queryKey: ["goverment_category"],
queryFn: () => eservices_api.category(),
});
const handleOpenBrowser = async (fileUrl: string) => {
try {
await WebBrowser.openBrowserAsync(fileUrl, {
dismissButtonStyle: 'close',
presentationStyle: WebBrowser.WebBrowserPresentationStyle.FULL_SCREEN,
});
} catch (error) {
Toast.error(t("Xatolik yuz berdi"));
}
}
const onRefresh = async () => {
setRefreshing(true);
try {
await queryClient.refetchQueries({ queryKey: ['goverment_category'] });
await queryClient.refetchQueries({ queryKey: ["goverment_category"] });
} finally {
setRefreshing(false);
}
@@ -52,22 +67,21 @@ export default function EServicesCategoryScreen() {
const staticCategory = {
id: 0,
name: 'Express Diagnostika',
name: "Express Diagnostika",
image: Express_diagnistika,
url: 'https://myorg.uz/ru',
url: "https://myorg.uz/ru",
};
const categories = [staticCategory, ...(data?.data?.data?.results ?? [])];
const handlePress = (item: any) => {
if (item.id === 0 && item.url) {
setWebUrl(item.url);
setModalVisible(true);
handleOpenBrowser(item.url);
return;
}
router.push({
pathname: '/(dashboard)/e-service/e-services-category',
pathname: "/(dashboard)/e-service/e-services-category",
params: {
categoryId: item.id,
categoryName: item.name,
@@ -79,7 +93,7 @@ export default function EServicesCategoryScreen() {
<View
style={{
flex: 1,
backgroundColor: isDark ? dark.bg : '#f8fafc',
backgroundColor: isDark ? dark.bg : "#f8fafc",
padding: 16,
}}
>
@@ -91,8 +105,8 @@ export default function EServicesCategoryScreen() {
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor={isDark ? '#f8fafc' : '#020617'}
colors={['#3b82f6']}
tintColor={isDark ? "#f8fafc" : "#020617"}
colors={["#3b82f6"]}
/>
}
renderItem={({ item }) => (
@@ -101,13 +115,13 @@ export default function EServicesCategoryScreen() {
onPress={() => handlePress(item)}
style={{
marginHorizontal: 1,
backgroundColor: isDark ? '#FDFDFD' : '#ffffff',
backgroundColor: isDark ? "#FDFDFD" : "#ffffff",
borderRadius: 16,
flexDirection: 'row',
alignItems: 'center',
flexDirection: "row",
alignItems: "center",
borderWidth: isDark ? 1 : 0,
borderColor: isDark ? dark.border : 'transparent',
shadowColor: '#000',
borderColor: isDark ? dark.border : "transparent",
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.15,
shadowRadius: 4,
@@ -116,98 +130,31 @@ export default function EServicesCategoryScreen() {
>
<View
style={{
width: '100%',
width: "100%",
height: 100,
borderRadius: 12,
alignItems: 'center',
justifyContent: 'center',
alignItems: "center",
justifyContent: "center",
marginRight: 14,
padding: 2,
overflow: 'hidden',
overflow: "hidden",
}}
>
{item.image ? (
<Image
source={item.image}
style={{ width: '100%', height: '100%' }}
style={{ width: "100%", height: "100%" }}
contentFit="contain"
/>
) : (
<Text style={{ fontWeight: '700', color: dark.text }}>{item.name[0]}</Text>
<Text style={{ fontWeight: "700", color: dark.text }}>
{item.name[0]}
</Text>
)}
</View>
</TouchableOpacity>
)}
/>
<Modal visible={modalVisible} animationType="slide">
<SafeAreaView
style={{
flex: 1,
backgroundColor: isDark ? '#0f172a' : '#f8fafc',
}}
>
{/* WebView Header */}
<View
style={{
flexDirection: 'row',
paddingHorizontal: 16,
paddingVertical: 12,
backgroundColor: isDark ? '#1e293b' : '#f8fafc',
alignItems: 'center',
justifyContent: 'space-between',
borderBottomWidth: 1,
borderBottomColor: isDark ? '#334155' : '#e2e8f0',
}}
>
<TouchableOpacity
onPress={() => webviewRef.current?.goBack()}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<ChevronLeft size={28} color={isDark ? '#f1f5f9' : '#0f172a'} />
</TouchableOpacity>
<Text
style={{
flex: 1,
textAlign: 'center',
fontSize: 16,
fontWeight: '600',
color: isDark ? '#f1f5f9' : '#0f172a',
}}
numberOfLines={1}
>
{webUrl}
</Text>
<TouchableOpacity
onPress={() => setModalVisible(false)}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<XIcon size={28} color={isDark ? '#f1f5f9' : '#0f172a'} />
</TouchableOpacity>
</View>
{/* WebView */}
{webUrl && (
<WebView
ref={webviewRef}
originWhitelist={['*']}
javaScriptEnabled
domStorageEnabled
onShouldStartLoadWithRequest={(request) => {
// iOS va Android uchun barcha URLlarni WebView ichida ochish
return true;
}}
source={{ uri: webUrl }}
style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#f8fafc' }}
startInLoadingState
renderLoading={() => (
<ActivityIndicator color="#3b82f6" size="large" style={{ flex: 1 }} />
)}
/>
)}
</SafeAreaView>
</Modal>
</View>
);
}

View File

@@ -1,28 +1,26 @@
// EServicesScreen.tsx
import { useTheme } from '@/components/ThemeContext';
import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query';
import { Image } from 'expo-image';
import { router, useLocalSearchParams } from 'expo-router';
import { ChevronLeft, XIcon } from 'lucide-react-native';
import React, { useCallback, useRef, useState } from 'react';
import { useTheme } from "@/components/ThemeContext";
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
import { Image } from "expo-image";
import { router, useLocalSearchParams } from "expo-router";
import * as WebBrowser from "expo-web-browser";
import { ChevronLeft } from "lucide-react-native";
import { useCallback, useState } from "react";
import {
ActivityIndicator,
Dimensions,
FlatList,
Modal,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { WebView } from 'react-native-webview';
View
} from "react-native";
import { useTranslation } from 'react-i18next';
import { RefreshControl } from 'react-native-gesture-handler';
import { eservices_api } from '../lib/api';
import { useTranslation } from "react-i18next";
import { RefreshControl } from "react-native-gesture-handler";
import { Toast } from "toastify-react-native";
import { eservices_api } from "../lib/api";
const { width: SCREEN_WIDTH } = Dimensions.get('window');
const { width: SCREEN_WIDTH } = Dimensions.get("window");
const PAGE_SIZE = 10;
export interface GovermentServiceDataRes {
@@ -34,78 +32,99 @@ export interface GovermentServiceDataRes {
export default function EServicesScreen() {
const { isDark } = useTheme();
const [webUrl, setWebUrl] = useState<string | null>(null);
const [modalVisible, setModalVisible] = useState(false);
const webviewRef = useRef<WebView>(null);
const params = useLocalSearchParams();
const { t } = useTranslation();
const [refreshing, setRefreshing] = useState(false);
const queryClient = useQueryClient();
const { data, isLoading, isError, fetchNextPage, hasNextPage, isFetchingNextPage } =
useInfiniteQuery({
queryKey: ['goverment_service', params.categoryId],
queryFn: async ({ pageParam = 1 }) => {
const response = await eservices_api.list({
page: pageParam,
page_size: PAGE_SIZE,
category: Number(params.categoryId),
});
return response.data.data;
},
getNextPageParam: (lastPage) =>
lastPage.current_page < lastPage.total_pages ? lastPage.current_page + 1 : undefined,
initialPageParam: 1,
});
const {
data,
isLoading,
isError,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ["goverment_service", params.categoryId],
queryFn: async ({ pageParam = 1 }) => {
const response = await eservices_api.list({
page: pageParam,
page_size: PAGE_SIZE,
category: Number(params.categoryId),
});
return response.data.data;
},
getNextPageParam: (lastPage) =>
lastPage.current_page < lastPage.total_pages
? lastPage.current_page + 1
: undefined,
initialPageParam: 1,
});
const services: GovermentServiceDataRes[] = data?.pages.flatMap((p) => p.results) ?? [];
const services: GovermentServiceDataRes[] =
data?.pages.flatMap((p) => p.results) ?? [];
const onRefresh = async () => {
setRefreshing(true);
try {
await queryClient.refetchQueries({ queryKey: ['goverment_service'] });
await queryClient.refetchQueries({ queryKey: ["goverment_service"] });
} finally {
setRefreshing(false);
}
};
const openWebView = (url: string) => {
setWebUrl(url);
setModalVisible(true);
const handleOpenBrowser = async (fileUrl: string) => {
try {
await WebBrowser.openBrowserAsync(fileUrl);
} catch (error) {
Toast.error(t("Xatolik yuz berdi"));
}
};
const renderItem = useCallback(
({ item }: { item: GovermentServiceDataRes }) => (
<View style={{ alignItems: 'center', marginTop: 12, width: CARD_WIDTH }}>
{/* Logo (bosilganda WebView ochiladi) */}
<View style={{ alignItems: "center", marginTop: 12, width: CARD_WIDTH }}>
<TouchableOpacity
style={[
styles.card,
{ backgroundColor: isDark ? '#1e293b' : '#f8fafc' },
{ backgroundColor: isDark ? "#1e293b" : "#f8fafc" },
isDark ? styles.darkShadow : styles.lightShadow,
]}
onPress={() => openWebView(item.url)}
onPress={() => handleOpenBrowser(item.url)}
>
<Image source={{ uri: item.logo }} style={styles.logo} resizeMode="contain" />
<Image
source={{ uri: item.logo }}
style={styles.logo}
resizeMode="contain"
/>
</TouchableOpacity>
{/* Name (alogifa, faqat korsatish) */}
<Text
style={[
styles.name,
{ color: isDark ? '#f1f5f9' : '#0f172a', marginTop: 4, paddingHorizontal: 5 },
{
color: isDark ? "#f1f5f9" : "#0f172a",
marginTop: 4,
paddingHorizontal: 5,
},
]}
>
{item.name}
</Text>
</View>
),
[isDark]
[isDark],
);
if (isLoading) {
return (
<View style={[styles.center, { backgroundColor: isDark ? '#0f172a' : '#f8fafc' }]}>
<View
style={[
styles.center,
{ backgroundColor: isDark ? "#0f172a" : "#f8fafc" },
]}
>
<ActivityIndicator size="large" color="#3b82f6" />
</View>
);
@@ -113,40 +132,45 @@ export default function EServicesScreen() {
if (isError) {
return (
<View style={[styles.center, { backgroundColor: isDark ? '#0f172a' : '#f8fafc' }]}>
<Text style={{ color: 'red' }}>Xatolik yuz berdi</Text>
<View
style={[
styles.center,
{ backgroundColor: isDark ? "#0f172a" : "#f8fafc" },
]}
>
<Text style={{ color: "red" }}>Xatolik yuz berdi</Text>
</View>
);
}
return (
<View style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#f8fafc' }}>
<View style={{ flex: 1, backgroundColor: isDark ? "#0f172a" : "#f8fafc" }}>
{/* Header */}
<View
style={{
flexDirection: 'row',
alignItems: 'center',
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 16,
gap: 10,
paddingVertical: 12,
backgroundColor: isDark ? '#1e293b' : '#f8fafc',
backgroundColor: isDark ? "#1e293b" : "#f8fafc",
borderBottomWidth: 1,
borderBottomColor: isDark ? '#334155' : '#e2e8f0',
borderBottomColor: isDark ? "#334155" : "#e2e8f0",
}}
>
<TouchableOpacity
onPress={() => router.back()}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<ChevronLeft size={28} color={isDark ? '#f1f5f9' : '#0f172a'} />
<ChevronLeft size={28} color={isDark ? "#f1f5f9" : "#0f172a"} />
</TouchableOpacity>
<Text
style={{
flex: 1,
fontSize: 18,
fontWeight: '600',
color: isDark ? '#f1f5f9' : '#0f172a',
fontWeight: "600",
color: isDark ? "#f1f5f9" : "#0f172a",
}}
>
{params.categoryName}
@@ -158,18 +182,18 @@ export default function EServicesScreen() {
<View style={[styles.center, { flex: 1, padding: 16, gap: 5 }]}>
<Text
style={{
color: isDark ? '#f1f5f9' : '#0f172a',
color: isDark ? "#f1f5f9" : "#0f172a",
fontSize: 18,
textAlign: 'center',
textAlign: "center",
}}
>
{t("Bu kategoriya bo'yicha xizmat topilmadi")}
</Text>
<Text
style={{
color: isDark ? '#f1f5f9' : '#0f172a',
color: isDark ? "#f1f5f9" : "#0f172a",
fontSize: 16,
textAlign: 'center',
textAlign: "center",
}}
>
{t("Tez orada xizmat qo'shiladi")}
@@ -182,96 +206,34 @@ export default function EServicesScreen() {
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor={isDark ? '#f8fafc' : '#020617'}
colors={['#3b82f6']}
tintColor={isDark ? "#f8fafc" : "#020617"}
colors={["#3b82f6"]}
/>
}
keyExtractor={(item) => item.id.toString()}
renderItem={renderItem}
numColumns={3}
columnWrapperStyle={{ justifyContent: 'space-between', marginBottom: 12 }}
columnWrapperStyle={{
justifyContent: "space-between",
marginBottom: 12,
}}
contentContainerStyle={{ padding: 16, paddingBottom: 80 }}
onEndReached={() => hasNextPage && !isFetchingNextPage && fetchNextPage()}
onEndReached={() =>
hasNextPage && !isFetchingNextPage && fetchNextPage()
}
onEndReachedThreshold={0.4}
ListFooterComponent={
isFetchingNextPage ? (
<ActivityIndicator color="#3b82f6" size="large" style={{ marginVertical: 20 }} />
<ActivityIndicator
color="#3b82f6"
size="large"
style={{ marginVertical: 20 }}
/>
) : null
}
showsVerticalScrollIndicator={false}
/>
)}
{/* WebView Modal */}
<Modal visible={modalVisible} animationType="slide">
<SafeAreaView
style={{
flex: 1,
backgroundColor: isDark ? '#0f172a' : '#f8fafc',
}}
>
{/* WebView Header */}
<View
style={{
flexDirection: 'row',
paddingHorizontal: 16,
paddingVertical: 12,
backgroundColor: isDark ? '#1e293b' : '#f8fafc',
alignItems: 'center',
justifyContent: 'space-between',
borderBottomWidth: 1,
borderBottomColor: isDark ? '#334155' : '#e2e8f0',
}}
>
<TouchableOpacity
onPress={() => webviewRef.current?.goBack()}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<ChevronLeft size={28} color={isDark ? '#f1f5f9' : '#0f172a'} />
</TouchableOpacity>
<Text
style={{
flex: 1,
textAlign: 'center',
fontSize: 16,
fontWeight: '600',
color: isDark ? '#f1f5f9' : '#0f172a',
}}
numberOfLines={1}
>
{webUrl}
</Text>
<TouchableOpacity
onPress={() => setModalVisible(false)}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<XIcon size={28} color={isDark ? '#f1f5f9' : '#0f172a'} />
</TouchableOpacity>
</View>
{/* WebView */}
{webUrl && (
<WebView
ref={webviewRef}
originWhitelist={['*']}
javaScriptEnabled
domStorageEnabled
onShouldStartLoadWithRequest={(request) => {
// iOS va Android uchun barcha URLlarni WebView ichida ochish
return true;
}}
source={{ uri: webUrl }}
style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#f8fafc' }}
startInLoadingState
renderLoading={() => (
<ActivityIndicator color="#3b82f6" size="large" style={{ flex: 1 }} />
)}
/>
)}
</SafeAreaView>
</Modal>
</View>
);
}
@@ -279,12 +241,12 @@ export default function EServicesScreen() {
const CARD_WIDTH = (SCREEN_WIDTH - 50) / 3;
const styles = StyleSheet.create({
center: { flex: 1, justifyContent: 'center', alignItems: 'center' },
center: { flex: 1, justifyContent: "center", alignItems: "center" },
card: {
width: CARD_WIDTH,
borderRadius: 12,
padding: 12,
alignItems: 'center',
alignItems: "center",
},
logo: {
width: 80,
@@ -293,18 +255,18 @@ const styles = StyleSheet.create({
},
name: {
fontSize: 14,
fontWeight: '600',
textAlign: 'center',
fontWeight: "600",
textAlign: "center",
},
darkShadow: {
shadowColor: '#000',
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.4,
shadowRadius: 6,
elevation: 3,
},
lightShadow: {
shadowColor: '#000',
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,

View File

@@ -3,12 +3,12 @@ import { API_URLS } from '@/api/URLs';
import { ProductBody, ProductResponse } from '@/screens/home/lib/types';
import { AxiosResponse } from 'axios';
import {
ExployeesResponse,
MyAdsData,
MyAdsDataRes,
MyBonusesData,
NotificationListRes,
UserInfoResponseData,
ExployeesResponse,
MyAdsData,
MyAdsDataRes,
MyBonusesData,
NotificationListRes,
UserInfoResponseData,
} from './type';
export const user_api = {
@@ -61,10 +61,6 @@ export const user_api = {
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: {
page: number;
@@ -109,6 +105,11 @@ export const user_api = {
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 }) {
const res = await httpClient.get(API_URLS.My_Refferals, { params });
return res;

View File

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

View File

@@ -52,7 +52,7 @@ export function AnnouncementsTab() {
};
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 { data, isLoading, isError, fetchNextPage, hasNextPage, refetch } = useInfiniteQuery({
@@ -82,14 +82,14 @@ export function AnnouncementsTab() {
isLoading: loadingDetail,
isError: detailError,
} = useQuery({
queryKey: ['my_ads_id', selectedAnnouncement?.id],
queryFn: () => user_api.my_ads_detail(selectedAnnouncement?.id!),
queryKey: ['my_ads_id', selectedAnnouncement],
queryFn: () => user_api.ads_detail(Number(selectedAnnouncement)),
select: (res) => res.data.data,
enabled: !!selectedAnnouncement && sheetOpen,
enabled: !!selectedAnnouncement,
});
const openSheet = (item: MyAdsDataRes) => {
setSelectedAnnouncement(item);
setSelectedAnnouncement(item.id);
setSheetOpen(true);
requestAnimationFrame(() => bottomSheetRef.current?.present());
};
@@ -176,7 +176,7 @@ export function AnnouncementsTab() {
if (isError) {
return (
<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>
);
}
@@ -196,7 +196,7 @@ export function AnnouncementsTab() {
<FlatList
data={allAds}
keyExtractor={(item) => item.id.toString()}
contentContainerStyle={styles.list}
contentContainerStyle={[styles.list, { flexGrow: 1 }]}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={theme.primary} />
}
@@ -204,7 +204,7 @@ export function AnnouncementsTab() {
renderItem={({ item }) => (
<Pressable
style={[styles.card, { backgroundColor: theme.cardBg }]}
onPress={() => openSheet(item)}
onPress={() => { openSheet(item); setSelectedAnnouncement(item.id) }}
>
{item.files?.[0]?.file && (
<Image source={{ uri: item.files[0].file }} style={styles.cardImage} />
@@ -243,6 +243,14 @@ export function AnnouncementsTab() {
</View>
</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
@@ -253,14 +261,13 @@ export function AnnouncementsTab() {
backgroundStyle={{ backgroundColor: theme.sheetBg }}
handleIndicatorStyle={{ backgroundColor: theme.indicator }}
onDismiss={() => {
setSheetOpen(false);
setSelectedAnnouncement(null);
setSelectedAnnouncement(null); // shu yetarli
}}
>
<BottomSheetScrollView contentContainerStyle={styles.sheet}>
{loadingDetail && <ActivityIndicator size={'large'} />}
{detailError && (
<Text style={[styles.error, { color: theme.error }]}>{t('Xatolik yuz berdi')}</Text>
<Text style={[{ color: theme.error }]}>{t('Xatolik yuz berdi')}</Text>
)}
{detail && (
@@ -374,7 +381,7 @@ export function AnnouncementsTab() {
isDark ? styles.darkPaymentItem : styles.lightPaymentItem,
{ 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 }} />
</TouchableOpacity>
@@ -384,7 +391,7 @@ export function AnnouncementsTab() {
styles.paymentItem,
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]}>
{t('Referal orqali')}
@@ -498,6 +505,22 @@ const styles = StyleSheet.create({
fontWeight: '700',
},
loading: {},
error: {},
emptyContainer: {
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,
gap: 16,
paddingBottom: 30,
flexGrow: 1
},
card: {
borderRadius: 20,
@@ -236,6 +237,7 @@ const styles = StyleSheet.create({
fontWeight: '600' as const,
},
emptyContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 80,

View File

@@ -12,9 +12,9 @@ import {
Switch,
Text,
TextInput,
ToastAndroid,
View,
View
} from 'react-native';
import { Toast } from 'toastify-react-native';
import { user_api } from '../lib/api';
type FormType = {
@@ -44,12 +44,12 @@ export default function CreateReferrals() {
is_agent: boolean;
}) => user_api.create_referral(body),
onSuccess: () => {
ToastAndroid.show(t('Referral yaratildi'), ToastAndroid.SHORT);
Toast.success(t('Referral yaratildi'));
queryClient.refetchQueries({ queryKey: ['my_referrals'] });
router.back();
},
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 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)
e.description = 'Tavsif kamida 5 ta belgidan iborat bolishi kerak';

View File

@@ -151,7 +151,7 @@ export default function EditService() {
<ArrowLeft color={isDark ? '#fff' : '#0f172a'} />
</Pressable>
<Text style={[styles.headerTitle, { color: isDark ? '#fff' : '#0f172a' }]}>
{step === 1 ? t('Xizmatni tahrirlash (1/2)') : t('Xizmatni tahrirlash (2/2)')}
{t('Xizmatni tahrirlash')}
</Text>
</View>
<Pressable

View File

@@ -124,7 +124,7 @@ export function EmployeesTab() {
data={allEmployees}
keyExtractor={(item) => item.phone}
renderItem={renderItem}
contentContainerStyle={styles.list}
contentContainerStyle={[styles.list, { flexGrow: 1 }]}
onEndReached={() => {
if (hasNextPage && !isFetchingNextPage) {
fetchNextPage();
@@ -176,7 +176,12 @@ const styles = StyleSheet.create({
infoContainer: { flex: 1, gap: 4 },
name: { fontSize: 17, fontWeight: '700' },
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' },
emptyButton: {
flexDirection: 'row',

View File

@@ -68,9 +68,9 @@ export function ManualTab() {
/** 🔹 Video manbalari (SELECT ga bogliq) */
const videos = {
uz: require('@/assets/manual/manual_video_uz.webm'),
ru: require('@/assets/manual/manual_video_ru.webm'),
en: require('@/assets/manual/manual_video_en.webm'),
uz: require('@/assets/manual/manual_video_uz.mp4'),
ru: require('@/assets/manual/manual_video_ru.mp4'),
en: require('@/assets/manual/manual_video_en.mp4'),
};
const player = useVideoPlayer(videos[selectedLang], (player) => {
@@ -158,8 +158,6 @@ export function ManualTab() {
[imageLang]
);
const selectedLanguage = languages.find((l) => l.code === selectedLang);
useEffect(() => {
// listener qo'shish
const subscription = player.addListener('playingChange', (state) => {
@@ -350,7 +348,7 @@ const styles = StyleSheet.create({
container: { flex: 1 },
hero: { padding: 20 },
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 },
section: { marginBottom: 28 },

View File

@@ -116,7 +116,7 @@ export default function MyServicesScreen() {
<FlatList
data={allServices}
keyExtractor={(item) => item.id.toString()}
contentContainerStyle={styles.list}
contentContainerStyle={[styles.list, { flexGrow: 1 }]}
onEndReached={() => hasNextPage && fetchNextPage()}
refreshControl={
<RefreshControl
@@ -269,7 +269,7 @@ const styles = StyleSheet.create({
borderRadius: 12,
},
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 },
emptyButton: {
flexDirection: 'row',

View File

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

View File

@@ -260,7 +260,7 @@ export function ProductServicesTab() {
style={[
styles.categoryOptionText,
selectedCategories.includes(category) &&
styles.categoryOptionTextSelected,
styles.categoryOptionTextSelected,
]}
>
{category}
@@ -313,6 +313,7 @@ const styles = StyleSheet.create({
},
list: {
gap: 16,
flexGrow: 1
},
card: {
backgroundColor: '#1e293b',
@@ -372,6 +373,7 @@ const styles = StyleSheet.create({
fontWeight: '500' as const,
},
emptyContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 80,

View File

@@ -2,7 +2,7 @@ import { useTheme } from '@/components/ThemeContext';
import { useInfiniteQuery } from '@tanstack/react-query';
import * as Clipboard from 'expo-clipboard';
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 { useTranslation } from 'react-i18next';
import {
@@ -14,11 +14,11 @@ import {
Share,
StyleSheet,
Text,
ToastAndroid,
TouchableOpacity,
View,
View
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { Toast } from 'toastify-react-native';
import { user_api } from '../lib/api';
const PAGE_SIZE = 10;
@@ -68,7 +68,7 @@ export function ReferralsTab() {
// Clipboard + Share funksiyasi
const handleCopyAndShare = async (code: string) => {
const referralLink = `https://t.me/infotargetbot/join?startapp=${code}`;
const referralLink = `${code}`;
// Clipboard-ga nusxa olish
await Clipboard.setStringAsync(referralLink);
@@ -84,7 +84,7 @@ export function ReferralsTab() {
}
if (Platform.OS === 'android') {
ToastAndroid.show(t('Refferal kopiya qilindi'), ToastAndroid.SHORT);
Toast.success(t('Refferal kopiya qilindi'));
}
};
@@ -153,11 +153,22 @@ export function ReferralsTab() {
</View>
</View>
)}
ListEmptyComponent={
<Text style={{ textAlign: 'center', color: theme.subText }}>
{t('Refferallar topilmadi')}
</Text>
}
ListEmptyComponent={() => (
<View style={styles.emptyContainer}>
<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>
</View>
)}
/>
</View>
);
@@ -175,7 +186,7 @@ const styles = StyleSheet.create({
},
headerTitle: { fontSize: 18, fontWeight: '700' },
list: { padding: 16, gap: 12, paddingBottom: 30 },
list: { padding: 16, gap: 12, paddingBottom: 30, flexGrow: 1 },
card: {
borderRadius: 16,
@@ -217,4 +228,46 @@ const styles = StyleSheet.create({
amount: {
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,
},
});