Compare commits

...

10 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
49 changed files with 896 additions and 729 deletions

View File

@@ -28,7 +28,7 @@ export const API_URLS = {
User_Update: 'auth/user-update/', User_Update: 'auth/user-update/',
Employee_List: 'api/employee/', Employee_List: 'api/employee/',
My_Ads: 'api/my-ads/', My_Ads: 'api/my-ads/',
My_Ads_Detail: (id: number) => `api/my-ads/${id}`, Ads_Detail: (id: number) => `api/my-ads/${id}/`,
My_Bonuses: 'api/cashback/', My_Bonuses: 'api/cashback/',
My_Refferals: 'api/referral/', My_Refferals: 'api/referral/',
Goverment_Service: '/api/goverment-service/', Goverment_Service: '/api/goverment-service/',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

View File

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

View File

@@ -139,7 +139,7 @@
"Keyingi": "Next", "Keyingi": "Next",
"Xizmat sarlavhasi": "Service title", "Xizmat sarlavhasi": "Service title",
"Yangilashda xato yuz berdi": "Error occurred while updating", "Yangilashda xato yuz berdi": "Error occurred while updating",
"Xizmatni tahrirlash (1/2)": "Edit service (1/2)", "Xizmatni tahrirlash": "Edit service",
"Xizmatni tahrirlash (2/2)": "Edit service (2/2)", "Xizmatni tahrirlash (2/2)": "Edit service (2/2)",
"Tilni tanlang": "Select language", "Tilni tanlang": "Select language",
"Rejimni tanlang": "Select mode", "Rejimni tanlang": "Select mode",
@@ -226,5 +226,6 @@
"INN kiriting (9 raqam)": "Enter INN (9 digits)", "INN kiriting (9 raqam)": "Enter INN (9 digits)",
"Referal kodi": "Referral code", "Referal kodi": "Referral code",
"Direktor JSHSHR": "Director JSHSHR", "Direktor JSHSHR": "Director JSHSHR",
"Direktor JSHSHR (14 raqam)": "Director JSHSHR (number 14)" "Direktor JSHSHR (14 raqam)": "Director JSHSHR (number 14)",
"Hozircha bildirishnomalar yo'q": "No notifications yet"
} }

View File

@@ -139,7 +139,7 @@
"Keyingi": "Далее", "Keyingi": "Далее",
"Xizmat sarlavhasi": "Заголовок услуги", "Xizmat sarlavhasi": "Заголовок услуги",
"Yangilashda xato yuz berdi": "Произошла ошибка при обновлении", "Yangilashda xato yuz berdi": "Произошла ошибка при обновлении",
"Xizmatni tahrirlash (1/2)": "Редактирование услуги (1/2)", "Xizmatni tahrirlash": "Редактирование услуги",
"Xizmatni tahrirlash (2/2)": "Редактирование услуги (2/2)", "Xizmatni tahrirlash (2/2)": "Редактирование услуги (2/2)",
"Tilni tanlang": "Выберите язык", "Tilni tanlang": "Выберите язык",
"Rejimni tanlang": "Выберите режим", "Rejimni tanlang": "Выберите режим",
@@ -225,5 +225,6 @@
"INN kiriting (9 raqam)": "Введите ИНН (9 цифр)", "INN kiriting (9 raqam)": "Введите ИНН (9 цифр)",
"Referal kodi": "Реферальный код", "Referal kodi": "Реферальный код",
"Direktor JSHSHR": "Директор ПИНФЛ", "Direktor JSHSHR": "Директор ПИНФЛ",
"Direktor JSHSHR kiriting (14 raqam)": "Введите ПИНФЛ директора (14 цифр)" "Direktor JSHSHR kiriting (14 raqam)": "Введите ПИНФЛ директора (14 цифр)",
"Hozircha bildirishnomalar yo'q": "Пока нет уведомлений"
} }

View File

@@ -139,7 +139,7 @@
"Yangi xizmat (2/2)": "Yangi xizmat (2/2)", "Yangi xizmat (2/2)": "Yangi xizmat (2/2)",
"Keyingi": "Keyingi", "Keyingi": "Keyingi",
"Xizmat sarlavhasi": "Xizmat sarlavhasi", "Xizmat sarlavhasi": "Xizmat sarlavhasi",
"Xizmatni tahrirlash (1/2)": "Xizmatni tahrirlash (1/2)", "Xizmatni tahrirlash": "Xizmatni tahrirlash",
"Xizmatni tahrirlash (2/2)": "Xizmatni tahrirlash (2/2)", "Xizmatni tahrirlash (2/2)": "Xizmatni tahrirlash (2/2)",
"Tilni tanlang": "Tilni tanlang", "Tilni tanlang": "Tilni tanlang",
"Rejimni tanlang": "Rejimni tanlang", "Rejimni tanlang": "Rejimni tanlang",
@@ -177,6 +177,7 @@
"Faqat tasdiqlangan profillarga ishonch bilan murojaat qiling.": "Faqat tasdiqlangan profillarga ishonch bilan murojaat qiling.", "Faqat tasdiqlangan profillarga ishonch bilan murojaat qiling.": "Faqat tasdiqlangan profillarga ishonch bilan murojaat qiling.",
"Qo'llanma video": "Qo'llanma video", "Qo'llanma video": "Qo'llanma video",
"Bildirishnomalar": "Bildirishnomalar", "Bildirishnomalar": "Bildirishnomalar",
"Hozircha bildirishnomalar yo'q": "Hozircha bildirishnomalar yo'q",
"Bildirishnomalarni yuklashda muammo bo'ldi": "Bildirishnomalarni yuklashda muammo bo'ldi", "Bildirishnomalarni yuklashda muammo bo'ldi": "Bildirishnomalarni yuklashda muammo bo'ldi",
"Hozir": "Hozir", "Hozir": "Hozir",
"daqiqa oldin": "daqiqa oldin", "daqiqa oldin": "daqiqa oldin",

81
package-lock.json generated
View File

@@ -71,6 +71,7 @@
"react-native-worklets": "^0.5.1", "react-native-worklets": "^0.5.1",
"react-stately": "^3.39.0", "react-stately": "^3.39.0",
"tailwind-variants": "^0.1.20", "tailwind-variants": "^0.1.20",
"toastify-react-native": "^7.2.3",
"zustand": "^5.0.10" "zustand": "^5.0.10"
}, },
"devDependencies": { "devDependencies": {
@@ -15396,6 +15397,73 @@
"react-native-pager-view": ">= 6.0.0" "react-native-pager-view": ">= 6.0.0"
} }
}, },
"node_modules/react-native-vector-icons": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-10.3.0.tgz",
"integrity": "sha512-IFQ0RE57819hOUdFvgK4FowM5aMXg7C7XKsuGLevqXkkIJatc3QopN0wYrb2IrzUgmdpfP+QVIbI3S6h7M0btw==",
"deprecated": "react-native-vector-icons package has moved to a new model of per-icon-family packages. See the https://github.com/oblador/react-native-vector-icons/blob/master/MIGRATION.md on how to migrate",
"license": "MIT",
"dependencies": {
"prop-types": "^15.7.2",
"yargs": "^16.1.1"
},
"bin": {
"fa-upgrade.sh": "bin/fa-upgrade.sh",
"fa5-upgrade": "bin/fa5-upgrade.sh",
"fa6-upgrade": "bin/fa6-upgrade.sh",
"generate-icon": "bin/generate-icon.js"
}
},
"node_modules/react-native-vector-icons/node_modules/cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^7.0.0"
}
},
"node_modules/react-native-vector-icons/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/react-native-vector-icons/node_modules/yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
"license": "MIT",
"dependencies": {
"cliui": "^7.0.2",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.0",
"y18n": "^5.0.5",
"yargs-parser": "^20.2.2"
},
"engines": {
"node": ">=10"
}
},
"node_modules/react-native-vector-icons/node_modules/yargs-parser": {
"version": "20.2.9",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/react-native-web": { "node_modules/react-native-web": {
"version": "0.21.2", "version": "0.21.2",
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz", "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz",
@@ -17202,6 +17270,19 @@
"node": ">=8.0" "node": ">=8.0"
} }
}, },
"node_modules/toastify-react-native": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/toastify-react-native/-/toastify-react-native-7.2.3.tgz",
"integrity": "sha512-ngmpTKlTo0IRddwSsNWK+YKbB2veqotHy7Zpil4eksoLAlq0RPSgdVOk5QDEDUONJQ4r7ljGYeRW68KBztirsg==",
"license": "MIT",
"dependencies": {
"react-native-vector-icons": "*"
},
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/toidentifier": { "node_modules/toidentifier": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",

View File

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

View File

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

View File

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

View File

@@ -16,12 +16,12 @@ import {
Platform, Platform,
StyleSheet, StyleSheet,
Text, Text,
ToastAndroid,
TouchableOpacity, TouchableOpacity,
View, View
} from 'react-native'; } from 'react-native';
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { Toast } from 'toastify-react-native';
import { auth_api } from '../login/lib/api'; import { auth_api } from '../login/lib/api';
import useTokenStore from '../login/lib/hook'; import useTokenStore from '../login/lib/hook';
import ConfirmForm from './ConfirmForm'; import ConfirmForm from './ConfirmForm';
@@ -93,7 +93,7 @@ const RegisterConfirmScreen = () => {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
setResendTimer(60); setResendTimer(60);
if (Platform.OS === 'android') { if (Platform.OS === 'android') {
ToastAndroid.show(t('Kod qayta yuborildi'), ToastAndroid.SHORT); Toast.info(t('Kod qayta yuborildi'));
} }
}, },
onError: () => { onError: () => {

View File

@@ -11,20 +11,28 @@ import { AxiosError } from 'axios';
import { Image } from 'expo-image'; import { Image } from 'expo-image';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import { Building2, CheckIcon, Globe, Hash, Search, ShieldCheck, User, UserPlus } from 'lucide-react-native'; import {
Building2,
CheckIcon,
Globe,
Hash,
Search,
ShieldCheck,
User,
UserPlus,
} from 'lucide-react-native';
import React, { useCallback, useMemo, useRef, useState } from 'react'; import React, { useCallback, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
ActivityIndicator, ActivityIndicator,
KeyboardAvoidingView, Keyboard,
Platform,
ScrollView,
StyleSheet, StyleSheet,
Text, Text,
TextInput, TextInput,
TouchableOpacity, TouchableOpacity,
View, View
} from 'react-native'; } from 'react-native';
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { auth_api } from '../login/lib/api'; import { auth_api } from '../login/lib/api';
import PhonePrefix from '../login/ui/PhonePrefix'; import PhonePrefix from '../login/ui/PhonePrefix';
@@ -100,6 +108,7 @@ export default function RegisterFormScreen() {
}); });
const openCountrySheet = useCallback(() => { const openCountrySheet = useCallback(() => {
Keyboard.dismiss();
setTimeout(() => { setTimeout(() => {
countrySheetRef.current?.snapToIndex(0); countrySheetRef.current?.snapToIndex(0);
}, 100); }, 100);
@@ -125,15 +134,16 @@ export default function RegisterFormScreen() {
const selectedCountryName = useMemo(() => { const selectedCountryName = useMemo(() => {
if (!selectedCountry) return t('Tanlang'); if (!selectedCountry) return t('Tanlang');
return countryResponse?.find((c: any) => c.flag?.toUpperCase() === selectedCountry)?.name || t('Tanlang'); return (
countryResponse?.find((c: any) => c.flag?.toUpperCase() === selectedCountry)?.name ||
t('Tanlang')
);
}, [selectedCountry, countryResponse, t]); }, [selectedCountry, countryResponse, t]);
const filteredCountries = useMemo(() => { const filteredCountries = useMemo(() => {
if (!countrySearch.trim()) return countryResponse || []; if (!countrySearch.trim()) return countryResponse || [];
const q = countrySearch.toLowerCase().trim(); const q = countrySearch.toLowerCase().trim();
return (countryResponse || []).filter((c: any) => return (countryResponse || []).filter((c: any) => c.name?.toLowerCase().includes(q));
c.name?.toLowerCase().includes(q)
);
}, [countryResponse, countrySearch]); }, [countryResponse, countrySearch]);
const headerInfo = getHeaderInfo(personType); const headerInfo = getHeaderInfo(personType);
@@ -151,7 +161,6 @@ export default function RegisterFormScreen() {
onSuccess: (res) => { onSuccess: (res) => {
setInfo(res.data.data); setInfo(res.data.data);
setLoading(false); setLoading(false);
// INN o'zgarganda director ma'lumotlarini tozalash
setDirectorJshshr(''); setDirectorJshshr('');
setDirectorInfo(null); setDirectorInfo(null);
setErrorDirectorInfo(null); setErrorDirectorInfo(null);
@@ -169,29 +178,16 @@ export default function RegisterFormScreen() {
mutationFn: (body: { value: string }) => auth_api.get_director_info(body), mutationFn: (body: { value: string }) => auth_api.get_director_info(body),
onSuccess: (res) => { onSuccess: (res) => {
const directorData = res.data; const directorData = res.data;
const rows: Array<{ inn: string }> = directorData?.data?.entity?.name?.rows ?? [];
// -------------------------------------------------------
// INN TEKSHIRUVI
// Director response strukturasi:
// data.entity.name.rows = [{ inn: "123456789", ... }, ...]
// Shu rows ichida joriy inn bor-yo'qligini tekshiramiz
// -------------------------------------------------------
const rows: Array<{ inn: string }> =
directorData?.data?.entity?.name?.rows ?? [];
const innMatch = rows.some((row) => row.inn === inn); const innMatch = rows.some((row) => row.inn === inn);
if (!innMatch) { if (!innMatch) {
// Director bu tashkilotga tegishli emas
setDirectorInfo(null); setDirectorInfo(null);
setErrorDirectorInfo( setErrorDirectorInfo(t("Bu direktor ko'rsatilgan tashkilotga tegishli emas"));
t("Bu direktor ko'rsatilgan tashkilotga tegishli emas")
);
setDirectorLoading(false); setDirectorLoading(false);
return; return;
} }
// INN mos keldi — director ma'lumotini saqlash
setDirectorInfo(directorData); setDirectorInfo(directorData);
setDirectorLoading(false); setDirectorLoading(false);
setErrorDirectorInfo(null); setErrorDirectorInfo(null);
@@ -241,7 +237,10 @@ export default function RegisterFormScreen() {
const handlePassportSeriesChange = useCallback( const handlePassportSeriesChange = useCallback(
(text: string) => { (text: string) => {
const v = text.toUpperCase().replace(/[^A-Z]/g, '').slice(0, 2); const v = text
.toUpperCase()
.replace(/[^A-Z]/g, '')
.slice(0, 2);
setPassportSeries(v); setPassportSeries(v);
if (personType && passportNumber.length === 7 && jshshr.length === 14) { if (personType && passportNumber.length === 7 && jshshr.length === 14) {
setLoading(true); setLoading(true);
@@ -315,7 +314,7 @@ export default function RegisterFormScreen() {
inn.length === 9 && inn.length === 9 &&
info && info &&
directorJshshr.length === 14 && directorJshshr.length === 14 &&
directorInfo // faqat INN mos kelganda to'ldiriladi directorInfo
); );
} }
@@ -323,7 +322,6 @@ export default function RegisterFormScreen() {
})(); })();
const handleContinue = () => { const handleContinue = () => {
// Director to'liq ismi directorInfo dan olinadi
const directorFullName = isLegal const directorFullName = isLegal
? (directorInfo?.data?.entrepreneur?.rows[0]?.entrepreneur ?? '') ? (directorInfo?.data?.entrepreneur?.rows[0]?.entrepreneur ?? '')
: `${info?.firstname ?? ''} ${info?.lastname ?? ''} ${info?.middlename ?? ''}`.trim(); : `${info?.firstname ?? ''} ${info?.lastname ?? ''} ${info?.middlename ?? ''}`.trim();
@@ -350,29 +348,36 @@ export default function RegisterFormScreen() {
}; };
return ( return (
<View style={{ flex: 1 }}> // ✅ Fragment — BottomSheet tashqarida bo'lishi uchun
<View style={styles.container}> <>
<LinearGradient <KeyboardAwareScrollView
colors={['#0f172a', '#1e293b', '#334155']} enableOnAndroid
start={{ x: 0, y: 0 }} enableAutomaticScroll
end={{ x: 1, y: 1 }} extraScrollHeight={120}
style={StyleSheet.absoluteFill} style={styles.keyboardScroll}
/> >
<View style={styles.decorCircle1} /> {/* ✅ TouchableWithoutFeedback yo'q — onStartShouldSetResponder ishlatildi */}
<View style={styles.decorCircle2} /> <View
style={styles.container}
onStartShouldSetResponder={() => {
Keyboard.dismiss();
return false; // false — child elementlar (buttonlar) ham ishlaydi
}}
>
<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 /> <AuthHeader />
<SafeAreaView style={{ flex: 1 }} edges={['bottom']}> {/* ✅ SafeAreaView va ichki ScrollView yo'q — to'g'ridan View */}
<KeyboardAvoidingView <SafeAreaView style={{ flex: 1 }} edges={['bottom']}>
style={{ flex: 1 }} <View style={styles.scrollContent}>
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
<ScrollView
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
>
<View style={styles.header}> <View style={styles.header}>
<View style={styles.iconContainer}> <View style={styles.iconContainer}>
<LinearGradient <LinearGradient
@@ -394,43 +399,6 @@ export default function RegisterFormScreen() {
<View style={styles.card}> <View style={styles.card}>
<View style={styles.formGap}> <View style={styles.formGap}>
{/* ---- Davlat (Country) ---- */}
{/* <View>
<Text style={styles.label}>{t('Davlat')}</Text>
<TouchableOpacity
style={[
styles.input,
styles.inputDisabled,
]}
onPress={openCountrySheet}
activeOpacity={0.7}
testID="country-select"
disabled
>
{countryLoading ? (
<ActivityIndicator size="small" color="#3b82f6" style={{ flex: 1 }} />
) : (
<>
<Image
source={{ uri: `https://flagcdn.com/w320/${selectedCountry.toLowerCase()}.png` }}
style={{ width: 30, height: 15 }}
resizeMode="cover"
/>
<Text
style={[
styles.textInput,
// { color: selectedCountry === 'all' ? '#94a3b8' : '#1e293b' },
{ color: '#94a3b8' },
]}
numberOfLines={1}
>
{selectedCountryName}
</Text>
</>
)}
<ChevronDown size={18} color={'#cbd5e1'} />
</TouchableOpacity>
</View> */}
{/* ---- YATT / BAND ---- */} {/* ---- YATT / BAND ---- */}
{isYattOrBand && ( {isYattOrBand && (
<> <>
@@ -510,8 +478,9 @@ export default function RegisterFormScreen() {
)} )}
{/* ---- LEGAL ENTITY: Kompaniya ma'lumoti ---- */} {/* ---- LEGAL ENTITY: Kompaniya ma'lumoti ---- */}
{isLegal && info && ( {isLegal &&
hasValidName ? ( info &&
(hasValidName ? (
<View style={styles.infoBox}> <View style={styles.infoBox}>
<Text style={styles.infoLabel}>{t('Tashkilot')}</Text> <Text style={styles.infoLabel}>{t('Tashkilot')}</Text>
<Text style={styles.infoText}>{info.fullName || info.name}</Text> <Text style={styles.infoText}>{info.fullName || info.name}</Text>
@@ -527,10 +496,9 @@ export default function RegisterFormScreen() {
<View style={styles.errorBox}> <View style={styles.errorBox}>
<Text style={styles.errorText}>{t('Tashkilot topilmadi')}</Text> <Text style={styles.errorText}>{t('Tashkilot topilmadi')}</Text>
</View> </View>
) ))}
)}
{/* ---- LEGAL ENTITY: Direktor JSHSHR — faqat info kelganda ko'rinadi ---- */} {/* ---- LEGAL ENTITY: Direktor JSHSHR ---- */}
{isLegal && info && ( {isLegal && info && (
<View> <View>
<Text style={styles.label}>{t('Direktor JSHSHR')}</Text> <Text style={styles.label}>{t('Direktor JSHSHR')}</Text>
@@ -551,7 +519,6 @@ export default function RegisterFormScreen() {
)} )}
</View> </View>
{/* ---- Direktor ma'lumoti (INN mos kelgan holda) ---- */}
{directorInfo && ( {directorInfo && (
<View style={[styles.infoBox, { marginTop: 8 }]}> <View style={[styles.infoBox, { marginTop: 8 }]}>
<Text style={styles.infoLabel}>{t('Direktor')}</Text> <Text style={styles.infoLabel}>{t('Direktor')}</Text>
@@ -561,7 +528,6 @@ export default function RegisterFormScreen() {
</View> </View>
)} )}
{/* ---- Xato: INN mos kelmasa yoki server xatosi ---- */}
{directorInfoError && ( {directorInfoError && (
<View style={[styles.errorBox, { marginTop: 8 }]}> <View style={[styles.errorBox, { marginTop: 8 }]}>
<Text style={styles.errorText}>{directorInfoError}</Text> <Text style={styles.errorText}>{directorInfoError}</Text>
@@ -577,11 +543,11 @@ export default function RegisterFormScreen() {
<Hash size={18} color="#94a3b8" /> <Hash size={18} color="#94a3b8" />
<TextInput <TextInput
value={referal} value={referal}
keyboardType="numeric"
placeholder={t('Referal kodi')} placeholder={t('Referal kodi')}
placeholderTextColor="#94a3b8" placeholderTextColor="#94a3b8"
style={styles.textInput} style={styles.textInput}
onChangeText={setReferal} onChangeText={setReferal}
maxLength={9}
testID="referal-input" testID="referal-input"
/> />
</View> </View>
@@ -605,8 +571,9 @@ export default function RegisterFormScreen() {
</View> </View>
{/* ---- YATT/BAND info ---- */} {/* ---- YATT/BAND info ---- */}
{!isLegal && info && ( {!isLegal &&
hasValidName ? ( info &&
(hasValidName ? (
<View style={styles.infoBox}> <View style={styles.infoBox}>
<Text style={styles.infoText}>{info.fullName || info.name}</Text> <Text style={styles.infoText}>{info.fullName || info.name}</Text>
</View> </View>
@@ -620,8 +587,7 @@ export default function RegisterFormScreen() {
<View style={styles.errorBox}> <View style={styles.errorBox}>
<Text style={styles.errorText}>{t('Foydalanuvchi topilmadi')}</Text> <Text style={styles.errorText}>{t('Foydalanuvchi topilmadi')}</Text>
</View> </View>
) ))}
)}
{/* ---- Davom etish tugmasi ---- */} {/* ---- Davom etish tugmasi ---- */}
<TouchableOpacity <TouchableOpacity
@@ -634,101 +600,103 @@ export default function RegisterFormScreen() {
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
</ScrollView> </View>
</KeyboardAvoidingView> </SafeAreaView>
</SafeAreaView> </View>
</KeyboardAwareScrollView>
{/* ---- Country BottomSheet ---- */} {/* BottomSheet KeyboardAwareScrollView TASHQARISIDA */}
<BottomSheet <BottomSheet
ref={countrySheetRef} ref={countrySheetRef}
index={-1} index={-1}
snapPoints={snapPoints} snapPoints={snapPoints}
enablePanDownToClose={true} enablePanDownToClose={true}
enableDynamicSizing={false} enableDynamicSizing={false}
enableOverDrag={false} enableOverDrag={false}
backdropComponent={renderBackdrop} backdropComponent={renderBackdrop}
backgroundStyle={styles.bottomSheetBg} backgroundStyle={styles.bottomSheetBg}
handleIndicatorStyle={styles.handleIndicator} handleIndicatorStyle={styles.handleIndicator}
android_keyboardInputMode="adjustResize" android_keyboardInputMode="adjustResize"
keyboardBehavior="interactive" keyboardBehavior="interactive"
keyboardBlurBehavior="restore" keyboardBlurBehavior="restore"
> >
<View style={styles.sheetHeader}> <View style={styles.sheetHeader}>
<Text style={styles.sheetTitle}>{t('Davlat')}</Text> <Text style={styles.sheetTitle}>{t('Davlat')}</Text>
</View> </View>
{/* Search input */} <View style={styles.searchContainer}>
<View style={styles.searchContainer}> <Search size={16} color="#94a3b8" />
<Search size={16} color="#94a3b8" /> <BottomSheetTextInput
<BottomSheetTextInput value={countrySearch}
value={countrySearch} onChangeText={setCountrySearch}
onChangeText={setCountrySearch} placeholder={t('Qidirish...')}
placeholder={t('Qidirish...')} placeholderTextColor="#94a3b8"
placeholderTextColor="#94a3b8" style={styles.searchInput}
style={styles.searchInput} clearButtonMode="while-editing"
clearButtonMode="while-editing" autoCorrect={false}
autoCorrect={false}
/>
</View>
<BottomSheetFlatList
data={filteredCountries}
keyExtractor={(item: any) => item.id?.toString()}
contentContainerStyle={styles.listContainer}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
ListEmptyComponent={
<View style={styles.emptyList}>
<Text style={styles.emptyListText}>{t('Natija topilmadi')}</Text>
</View>
}
renderItem={({ item }: { item: any }) => {
const isSelected = item.flag?.toUpperCase() === selectedCountry;
const flagCode = item.flag ? item.flag.toLowerCase() : '';
return (
<TouchableOpacity
style={[
styles.listItem,
isSelected && styles.selectedListItem,
]}
onPress={() => {
setSelectedCountry(item.flag?.toUpperCase());
closeCountrySheet();
}}
activeOpacity={0.7}
>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 10, flex: 1 }}>
{flagCode ? (
<Image
source={{ uri: `https://flagcdn.com/w320/${flagCode}.png` }}
style={{ width: 34, height: 22, borderRadius: 2 }}
/>
) : (
<Globe size={20} color={isSelected ? '#2563eb' : '#94a3b8'} />
)}
<Text style={[styles.listItemText, isSelected && styles.selectedListItemText]}>
{item.name}
</Text>
</View>
{isSelected && (
<View style={styles.checkmark}>
<CheckIcon color="#3b82f6" strokeWidth={2.5} size={16} />
</View>
)}
</TouchableOpacity>
);
}}
/> />
</BottomSheet> </View>
</View>
</View> <BottomSheetFlatList
data={filteredCountries}
keyExtractor={(item: any) => item.id?.toString()}
contentContainerStyle={styles.listContainer}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
ListEmptyComponent={
<View style={styles.emptyList}>
<Text style={styles.emptyListText}>{t('Natija topilmadi')}</Text>
</View>
}
renderItem={({ item }: { item: any }) => {
const isSelected = item.flag?.toUpperCase() === selectedCountry;
const flagCode = item.flag ? item.flag.toLowerCase() : '';
return (
<TouchableOpacity
style={[styles.listItem, isSelected && styles.selectedListItem]}
onPress={() => {
setSelectedCountry(item.flag?.toUpperCase());
closeCountrySheet();
}}
activeOpacity={0.7}
>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 10, flex: 1 }}>
{flagCode ? (
<Image
source={{ uri: `https://flagcdn.com/w320/${flagCode}.png` }}
style={{ width: 34, height: 22, borderRadius: 2 }}
/>
) : (
<Globe size={20} color={isSelected ? '#2563eb' : '#94a3b8'} />
)}
<Text style={[styles.listItemText, isSelected && styles.selectedListItemText]}>
{item.name}
</Text>
</View>
{isSelected && (
<View style={styles.checkmark}>
<CheckIcon color="#3b82f6" strokeWidth={2.5} size={16} />
</View>
)}
</TouchableOpacity>
);
}}
/>
</BottomSheet>
</>
); );
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
// ✅ KeyboardAwareScrollView uchun style
keyboardScroll: {
flex: 1,
backgroundColor: '#0f172a',
},
container: { container: {
flex: 1, flex: 1,
backgroundColor: '#0f172a', backgroundColor: '#0f172a',
minHeight: '100%',
}, },
scrollContent: { scrollContent: {
flexGrow: 1, flexGrow: 1,
@@ -958,15 +926,6 @@ const styles = StyleSheet.create({
fontWeight: '800' as const, fontWeight: '800' as const,
fontSize: 16, fontSize: 16,
}, },
footer: {
marginTop: 20,
alignItems: 'center',
},
footerText: {
color: '#94a3b8',
fontSize: 14,
fontWeight: '600' as const,
},
decorCircle1: { decorCircle1: {
position: 'absolute', position: 'absolute',
top: -150, top: -150,
@@ -986,7 +945,7 @@ const styles = StyleSheet.create({
backgroundColor: 'rgba(16, 185, 129, 0.08)', backgroundColor: 'rgba(16, 185, 129, 0.08)',
}, },
inputDisabled: { inputDisabled: {
backgroundColor: '#f1f5f9', // disabled bo'lganda fon rangi backgroundColor: '#f1f5f9',
borderColor: '#e2e8f0', // disabled bo'lganda border rangi borderColor: '#e2e8f0',
}, },
}); });

View File

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

View File

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

View File

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

View File

@@ -1,42 +1,57 @@
import Express_diagnistika from '@/assets/images/Express_diagnistika.png'; import Express_diagnistika from "@/assets/images/Express_diagnistika.png";
import { useTheme } from '@/components/ThemeContext'; import { useTheme } from "@/components/ThemeContext";
import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useQuery, useQueryClient } from "@tanstack/react-query";
import { Image } from 'expo-image'; import { Image } from "expo-image";
import { router } from 'expo-router'; import { router } from "expo-router";
import { ChevronLeft, XIcon } from 'lucide-react-native'; import * as WebBrowser from "expo-web-browser";
import React, { useRef, useState } from 'react'; import { useState } from "react";
import { ActivityIndicator, FlatList, Modal, Text, TouchableOpacity, View } from 'react-native'; import { useTranslation } from "react-i18next";
import { RefreshControl } from 'react-native-gesture-handler'; import {
import { SafeAreaView } from 'react-native-safe-area-context'; ActivityIndicator,
import { WebView } from 'react-native-webview'; FlatList,
import { eservices_api } from '../lib/api'; 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 = { const dark = {
bg: '#0f172a', bg: "#0f172a",
card: '#334155', card: "#334155",
border: '#1e293b', border: "#1e293b",
muted: '#334155', muted: "#334155",
text: '#E5B037', text: "#E5B037",
subText: '#0B0F2C', subText: "#0B0F2C",
}; };
export default function EServicesCategoryScreen() { export default function EServicesCategoryScreen() {
const { isDark } = useTheme(); 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 [refreshing, setRefreshing] = useState(false);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { t } = useTranslation();
const { data, isLoading } = useQuery({ const { data, isLoading } = useQuery({
queryKey: ['goverment_category'], queryKey: ["goverment_category"],
queryFn: () => eservices_api.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 () => { const onRefresh = async () => {
setRefreshing(true); setRefreshing(true);
try { try {
await queryClient.refetchQueries({ queryKey: ['goverment_category'] }); await queryClient.refetchQueries({ queryKey: ["goverment_category"] });
} finally { } finally {
setRefreshing(false); setRefreshing(false);
} }
@@ -52,22 +67,21 @@ export default function EServicesCategoryScreen() {
const staticCategory = { const staticCategory = {
id: 0, id: 0,
name: 'Express Diagnostika', name: "Express Diagnostika",
image: Express_diagnistika, image: Express_diagnistika,
url: 'https://myorg.uz/ru', url: "https://myorg.uz/ru",
}; };
const categories = [staticCategory, ...(data?.data?.data?.results ?? [])]; const categories = [staticCategory, ...(data?.data?.data?.results ?? [])];
const handlePress = (item: any) => { const handlePress = (item: any) => {
if (item.id === 0 && item.url) { if (item.id === 0 && item.url) {
setWebUrl(item.url); handleOpenBrowser(item.url);
setModalVisible(true);
return; return;
} }
router.push({ router.push({
pathname: '/(dashboard)/e-service/e-services-category', pathname: "/(dashboard)/e-service/e-services-category",
params: { params: {
categoryId: item.id, categoryId: item.id,
categoryName: item.name, categoryName: item.name,
@@ -79,7 +93,7 @@ export default function EServicesCategoryScreen() {
<View <View
style={{ style={{
flex: 1, flex: 1,
backgroundColor: isDark ? dark.bg : '#f8fafc', backgroundColor: isDark ? dark.bg : "#f8fafc",
padding: 16, padding: 16,
}} }}
> >
@@ -91,8 +105,8 @@ export default function EServicesCategoryScreen() {
<RefreshControl <RefreshControl
refreshing={refreshing} refreshing={refreshing}
onRefresh={onRefresh} onRefresh={onRefresh}
tintColor={isDark ? '#f8fafc' : '#020617'} tintColor={isDark ? "#f8fafc" : "#020617"}
colors={['#3b82f6']} colors={["#3b82f6"]}
/> />
} }
renderItem={({ item }) => ( renderItem={({ item }) => (
@@ -101,13 +115,13 @@ export default function EServicesCategoryScreen() {
onPress={() => handlePress(item)} onPress={() => handlePress(item)}
style={{ style={{
marginHorizontal: 1, marginHorizontal: 1,
backgroundColor: isDark ? '#FDFDFD' : '#ffffff', backgroundColor: isDark ? "#FDFDFD" : "#ffffff",
borderRadius: 16, borderRadius: 16,
flexDirection: 'row', flexDirection: "row",
alignItems: 'center', alignItems: "center",
borderWidth: isDark ? 1 : 0, borderWidth: isDark ? 1 : 0,
borderColor: isDark ? dark.border : 'transparent', borderColor: isDark ? dark.border : "transparent",
shadowColor: '#000', shadowColor: "#000",
shadowOffset: { width: 0, height: 2 }, shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.15, shadowOpacity: 0.15,
shadowRadius: 4, shadowRadius: 4,
@@ -116,98 +130,31 @@ export default function EServicesCategoryScreen() {
> >
<View <View
style={{ style={{
width: '100%', width: "100%",
height: 100, height: 100,
borderRadius: 12, borderRadius: 12,
alignItems: 'center', alignItems: "center",
justifyContent: 'center', justifyContent: "center",
marginRight: 14, marginRight: 14,
padding: 2, padding: 2,
overflow: 'hidden', overflow: "hidden",
}} }}
> >
{item.image ? ( {item.image ? (
<Image <Image
source={item.image} source={item.image}
style={{ width: '100%', height: '100%' }} style={{ width: "100%", height: "100%" }}
contentFit="contain" 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> </View>
</TouchableOpacity> </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>
); );
} }

View File

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

View File

@@ -3,12 +3,12 @@ import { API_URLS } from '@/api/URLs';
import { ProductBody, ProductResponse } from '@/screens/home/lib/types'; import { ProductBody, ProductResponse } from '@/screens/home/lib/types';
import { AxiosResponse } from 'axios'; import { AxiosResponse } from 'axios';
import { import {
ExployeesResponse, ExployeesResponse,
MyAdsData, MyAdsData,
MyAdsDataRes, MyAdsDataRes,
MyBonusesData, MyBonusesData,
NotificationListRes, NotificationListRes,
UserInfoResponseData, UserInfoResponseData,
} from './type'; } from './type';
export const user_api = { export const user_api = {
@@ -61,10 +61,6 @@ export const user_api = {
return res; return res;
}, },
async my_ads_detail(id: number): Promise<AxiosResponse<{ status: boolean; data: MyAdsDataRes }>> {
const res = await httpClient.get(API_URLS.My_Ads_Detail(id));
return res;
},
async my_bonuses(params: { async my_bonuses(params: {
page: number; page: number;
@@ -109,6 +105,11 @@ export const user_api = {
return res; return res;
}, },
async ads_detail(id: number): Promise<AxiosResponse<{ status: boolean; data: MyAdsDataRes }>> {
const res = await httpClient.get(API_URLS.Ads_Detail(id));
return res;
},
async my_referrals(params: { page: number; page_size: number }) { async my_referrals(params: { page: number; page_size: number }) {
const res = await httpClient.get(API_URLS.My_Refferals, { params }); const res = await httpClient.get(API_URLS.My_Refferals, { params });
return res; return res;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -91,32 +91,77 @@ export function NotificationTab() {
<Text style={[styles.headerTitle, { color: isDark ? '#f1f5f9' : '#0f172a' }]}> <Text style={[styles.headerTitle, { color: isDark ? '#f1f5f9' : '#0f172a' }]}>
{t('Bildirishnomalar')} {t('Bildirishnomalar')}
</Text> </Text>
{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> </View>
<FlatList <FlatList
data={notifications} data={notifications}
keyExtractor={(item) => item.id.toString()} keyExtractor={(item) => item.id.toString()}
contentContainerStyle={styles.listContent} contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false} 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={() => { onEndReached={() => {
if (hasNextPage && !isFetchingNextPage) { if (hasNextPage && !isFetchingNextPage) {
fetchNextPage(); fetchNextPage();
@@ -130,6 +175,15 @@ export function NotificationTab() {
} }
refreshing={isLoading} refreshing={isLoading}
onRefresh={refetch} onRefresh={refetch}
ListEmptyComponent={() =>
!isLoading && (
<View style={styles.emptyContainer}>
<Text style={[styles.emptyTitle, { color: isDark ? '#f1f5f9' : '#0f172a' }]}>
{t("Hozircha bildirishnomalar yo'q")}
</Text>
</View>
)
}
/> />
</View> </View>
); );
@@ -266,6 +320,7 @@ const styles = StyleSheet.create({
listContent: { listContent: {
padding: 16, padding: 16,
paddingBottom: 32, paddingBottom: 32,
flexGrow: 1
}, },
/* Card Styles */ /* Card Styles */
@@ -292,6 +347,38 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center',
}, },
headerActions: {
marginBottom: 16,
alignItems: 'flex-end',
},
emptyHeader: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 60,
},
allReadContainer: {
marginBottom: 16,
alignItems: 'center',
},
allReadText: {
fontSize: 14,
},
emptyTitle: {
fontSize: 18,
fontWeight: '700',
marginBottom: 8,
textAlign: 'center',
},
emptyDesc: {
fontSize: 14,
textAlign: 'center',
},
cardHeader: { cardHeader: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
@@ -407,7 +494,7 @@ const styles = StyleSheet.create({
paddingVertical: 8, paddingVertical: 8,
borderRadius: 8, borderRadius: 8,
borderWidth: 1, borderWidth: 1,
minWidth: 80, width: "auto",
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}, },
@@ -415,4 +502,10 @@ const styles = StyleSheet.create({
fontSize: 13, fontSize: 13,
fontWeight: '600', fontWeight: '600',
}, },
emptyContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 30,
},
}); });

View File

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

View File

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