complated project
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
||||
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { auth_api } from '../login/lib/api';
|
||||
import useTokenStore from '../login/lib/hook';
|
||||
import ConfirmForm from './ConfirmForm';
|
||||
|
||||
const ConfirmScreen = () => {
|
||||
@@ -28,6 +29,7 @@ const ConfirmScreen = () => {
|
||||
const [phoneOTP, setPhone] = useState<string | null>('');
|
||||
const [error, setError] = useState<string>('');
|
||||
const { login } = useAuth();
|
||||
const { savedToken } = useTokenStore();
|
||||
|
||||
const [resendTimer, setResendTimer] = useState<number>(60);
|
||||
|
||||
@@ -59,6 +61,7 @@ const ConfirmScreen = () => {
|
||||
onSuccess: async (res) => {
|
||||
await AsyncStorage.removeItem('phone');
|
||||
await AsyncStorage.setItem('access_token', res.data.data.token.access);
|
||||
savedToken(res.data.data.token.access);
|
||||
await login(res.data.data.token.access);
|
||||
await AsyncStorage.setItem('refresh_token', res.data.data.token.refresh);
|
||||
router.replace('/(dashboard)');
|
||||
|
||||
16
screens/auth/login/lib/hook.ts
Normal file
16
screens/auth/login/lib/hook.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
type State = {
|
||||
token: string | null;
|
||||
};
|
||||
|
||||
type Actions = {
|
||||
savedToken: (token: string | null) => void;
|
||||
};
|
||||
|
||||
const useTokenStore = create<State & Actions>((set) => ({
|
||||
token: null,
|
||||
savedToken: (token: string | null) => set(() => ({ token })),
|
||||
}));
|
||||
|
||||
export default useTokenStore;
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { auth_api } from '../login/lib/api';
|
||||
import useTokenStore from '../login/lib/hook';
|
||||
import ConfirmForm from './ConfirmForm';
|
||||
|
||||
const RegisterConfirmScreen = () => {
|
||||
@@ -28,6 +29,7 @@ const RegisterConfirmScreen = () => {
|
||||
const [phoneOTP, setPhone] = useState<string | null>('');
|
||||
const [error, setError] = useState<string>('');
|
||||
const { login } = useAuth();
|
||||
const { savedToken } = useTokenStore();
|
||||
|
||||
const [resendTimer, setResendTimer] = useState<number>(60);
|
||||
|
||||
@@ -59,6 +61,7 @@ const RegisterConfirmScreen = () => {
|
||||
onSuccess: async (res) => {
|
||||
await AsyncStorage.removeItem('phone');
|
||||
await AsyncStorage.setItem('access_token', res.data.data.token.access);
|
||||
savedToken(res.data.data.token.access);
|
||||
await AsyncStorage.setItem('refresh_token', res.data.data.token.refresh);
|
||||
await login(res.data.data.token.access);
|
||||
router.replace('/(dashboard)');
|
||||
|
||||
@@ -209,7 +209,7 @@ export default function CreateAdsScreens() {
|
||||
behavior="padding"
|
||||
style={[styles.safeArea, isDark ? styles.darkBg : styles.lightBg]}
|
||||
>
|
||||
<ScrollView contentContainerStyle={styles.container}>
|
||||
<ScrollView contentContainerStyle={[styles.container, { paddingBottom: 90 }]}>
|
||||
<Text style={[styles.title, isDark ? styles.darkText : styles.lightText]}>
|
||||
{currentStep === 1
|
||||
? t("E'lon ma'lumotlari")
|
||||
@@ -235,43 +235,47 @@ export default function CreateAdsScreens() {
|
||||
/>
|
||||
)}
|
||||
{currentStep === 4 && <StepFour data={ads} setPayment={setPaymentType} />}
|
||||
<View style={styles.footer}>
|
||||
{currentStep > 1 && currentStep !== 4 && (
|
||||
<TouchableOpacity
|
||||
style={[styles.back, isDark ? styles.darkBack : styles.lightBack]}
|
||||
onPress={() => setCurrentStep((s) => s - 1)}
|
||||
>
|
||||
<Text style={[styles.btnText, isDark ? styles.darkBtnText : styles.lightBtnText]}>
|
||||
{t('Orqaga')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.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>
|
||||
</ScrollView>
|
||||
|
||||
{/* FOOTER */}
|
||||
<View style={styles.footer}>
|
||||
{currentStep > 1 && currentStep !== 4 && (
|
||||
<TouchableOpacity
|
||||
style={[styles.back, isDark ? styles.darkBack : styles.lightBack]}
|
||||
onPress={() => setCurrentStep((s) => s - 1)}
|
||||
>
|
||||
<Text style={[styles.btnText, isDark ? styles.darkBtnText : styles.lightBtnText]}>
|
||||
{t('Orqaga')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.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>
|
||||
|
||||
{/* PAYMENT BOTTOM SHEET */}
|
||||
<BottomSheetModal
|
||||
@@ -329,7 +333,7 @@ const styles = StyleSheet.create({
|
||||
lightBg: {
|
||||
backgroundColor: '#f8fafc',
|
||||
},
|
||||
container: { padding: 20, paddingBottom: 140 },
|
||||
container: { padding: 20 },
|
||||
title: { fontSize: 22, fontWeight: '800', marginBottom: 20 },
|
||||
darkText: {
|
||||
color: '#f1f5f9',
|
||||
@@ -339,12 +343,8 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
|
||||
footer: {
|
||||
position: 'absolute',
|
||||
bottom: 80,
|
||||
left: 20,
|
||||
right: 20,
|
||||
flexDirection: 'row',
|
||||
gap: 10,
|
||||
marginTop: 10,
|
||||
gap: 5,
|
||||
},
|
||||
back: {
|
||||
flex: 1,
|
||||
|
||||
14
screens/e-services/lib/api.ts
Normal file
14
screens/e-services/lib/api.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import httpClient from '@/api/httpClient';
|
||||
import { API_URLS } from '@/api/URLs';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { GovermentServiceData } from './types';
|
||||
|
||||
export const eservices_api = {
|
||||
async list(params: {
|
||||
page: number;
|
||||
page_size: number;
|
||||
}): Promise<AxiosResponse<GovermentServiceData>> {
|
||||
const res = await httpClient.get(API_URLS.Goverment_Service, { params });
|
||||
return res;
|
||||
},
|
||||
};
|
||||
21
screens/e-services/lib/types.ts
Normal file
21
screens/e-services/lib/types.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export interface GovermentServiceData {
|
||||
status: boolean;
|
||||
data: {
|
||||
links: {
|
||||
previous: null | string;
|
||||
next: null | string;
|
||||
};
|
||||
total_items: number;
|
||||
total_pages: number;
|
||||
page_size: number;
|
||||
current_page: number;
|
||||
results: GovermentServiceDataRes[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface GovermentServiceDataRes {
|
||||
id: number;
|
||||
name: string;
|
||||
url: string;
|
||||
logo: string;
|
||||
}
|
||||
199
screens/e-services/ui/EServices.tsx
Normal file
199
screens/e-services/ui/EServices.tsx
Normal file
@@ -0,0 +1,199 @@
|
||||
// EServicesScreen.tsx
|
||||
import { useTheme } from '@/components/ThemeContext';
|
||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { Image } from 'expo-image';
|
||||
import { ChevronLeft, XIcon } from 'lucide-react-native';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Dimensions,
|
||||
FlatList,
|
||||
Modal,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { WebView } from 'react-native-webview';
|
||||
import { eservices_api } from '../lib/api';
|
||||
|
||||
const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
export interface GovermentServiceDataRes {
|
||||
id: number;
|
||||
name: string;
|
||||
url: string;
|
||||
logo: string;
|
||||
}
|
||||
|
||||
export default function EServicesScreen() {
|
||||
const { isDark } = useTheme();
|
||||
const [webUrl, setWebUrl] = useState<string | null>(null);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const webviewRef = useRef<WebView>(null); // WebView ref for goBack
|
||||
|
||||
const { data, isLoading, isError, fetchNextPage, hasNextPage, isFetchingNextPage } =
|
||||
useInfiniteQuery({
|
||||
queryKey: ['goverment_service'],
|
||||
queryFn: async ({ pageParam = 1 }) => {
|
||||
const response = await eservices_api.list({
|
||||
page: pageParam,
|
||||
page_size: PAGE_SIZE,
|
||||
});
|
||||
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 openWebView = (url: string) => {
|
||||
setWebUrl(url);
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const renderItem = useCallback(
|
||||
({ item }: { item: GovermentServiceDataRes }) => (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.card,
|
||||
{ backgroundColor: isDark ? '#1e293b' : '#f8fafc' },
|
||||
isDark ? styles.darkShadow : styles.lightShadow,
|
||||
]}
|
||||
onPress={() => openWebView(item.url)}
|
||||
>
|
||||
<Image source={{ uri: item.logo }} style={styles.logo} resizeMode="contain" />
|
||||
<Text style={[styles.name, { color: isDark ? '#f1f5f9' : '#0f172a' }]}>{item.name}</Text>
|
||||
</TouchableOpacity>
|
||||
),
|
||||
[isDark]
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View style={[styles.center, { backgroundColor: isDark ? '#0f172a' : '#f8fafc' }]}>
|
||||
<ActivityIndicator size="large" color="#3b82f6" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<View style={[styles.center, { backgroundColor: isDark ? '#0f172a' : '#f8fafc' }]}>
|
||||
<Text style={{ color: 'red' }}>Xatolik yuz berdi</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#f8fafc' }}>
|
||||
<FlatList
|
||||
data={services}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
renderItem={renderItem}
|
||||
numColumns={2}
|
||||
columnWrapperStyle={{ justifyContent: 'space-between', marginBottom: 12 }}
|
||||
contentContainerStyle={{ padding: 16 }}
|
||||
onEndReached={() => hasNextPage && fetchNextPage()}
|
||||
onEndReachedThreshold={0.4}
|
||||
ListFooterComponent={
|
||||
isFetchingNextPage ? <ActivityIndicator color="#3b82f6" style={{ margin: 20 }} /> : null
|
||||
}
|
||||
showsVerticalScrollIndicator={false}
|
||||
/>
|
||||
|
||||
{/* WebView Modal */}
|
||||
{/* WebView Modal */}
|
||||
<Modal visible={modalVisible} animationType="slide">
|
||||
<SafeAreaView
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: isDark ? '#0f172a' : '#f8fafc', // modal background
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
padding: 12,
|
||||
backgroundColor: isDark ? '#1e293b' : '#f8fafc', // header background
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
{/* Back tugmasi */}
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (webviewRef.current) webviewRef.current.goBack();
|
||||
}}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
>
|
||||
<ChevronLeft color={isDark ? '#f1f5f9' : '#0f172a'} size={24} />
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Close tugmasi */}
|
||||
<TouchableOpacity
|
||||
onPress={() => setModalVisible(false)}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
>
|
||||
<XIcon color={isDark ? '#f1f5f9' : '#0f172a'} size={24} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* WebView */}
|
||||
{webUrl && (
|
||||
<WebView
|
||||
ref={webviewRef}
|
||||
source={{ uri: webUrl }}
|
||||
style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#f8fafc' }} // webview background
|
||||
startInLoadingState
|
||||
renderLoading={() => (
|
||||
<ActivityIndicator color="#3b82f6" size="large" style={{ flex: 1 }} />
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</SafeAreaView>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const CARD_WIDTH = (SCREEN_WIDTH - 48) / 2;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
center: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
card: {
|
||||
width: CARD_WIDTH,
|
||||
borderRadius: 12,
|
||||
padding: 12,
|
||||
alignItems: 'center',
|
||||
},
|
||||
logo: {
|
||||
width: 60,
|
||||
height: 60,
|
||||
marginBottom: 8,
|
||||
},
|
||||
name: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
},
|
||||
darkShadow: {
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.4,
|
||||
shadowRadius: 6,
|
||||
elevation: 3,
|
||||
},
|
||||
lightShadow: {
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 2,
|
||||
},
|
||||
});
|
||||
@@ -79,6 +79,7 @@ export interface CountryBody {
|
||||
export interface CountryResponse {
|
||||
id: number;
|
||||
name: string;
|
||||
flag: string;
|
||||
companies: {
|
||||
id: number;
|
||||
company_name: string;
|
||||
|
||||
@@ -15,6 +15,7 @@ import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Modal,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
@@ -22,7 +23,8 @@ import {
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { RefreshControl } from 'react-native-gesture-handler';
|
||||
import { GestureHandlerRootView, RefreshControl } from 'react-native-gesture-handler';
|
||||
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
|
||||
|
||||
function Loading() {
|
||||
return (
|
||||
@@ -84,83 +86,96 @@ export default function HomeScreen() {
|
||||
}
|
||||
}, [activeTab, query]);
|
||||
|
||||
if (showFilter && step === 'filter') {
|
||||
return (
|
||||
<FilterUI back={() => setShowFilter(false)} setStep={setStep} setFiltered={setFiltered} />
|
||||
);
|
||||
}
|
||||
const handleCloseFilter = () => {
|
||||
setShowFilter(false);
|
||||
setStep('filter');
|
||||
};
|
||||
|
||||
// Show filtered items if filter was applied
|
||||
if (showFilter && step === 'items') {
|
||||
return (
|
||||
<FilteredItems
|
||||
data={filtered}
|
||||
back={() => {
|
||||
setShowFilter(false);
|
||||
setStep('filter');
|
||||
}}
|
||||
/>
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<FilteredItems data={filtered} back={handleCloseFilter} />
|
||||
</GestureHandlerRootView>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={[isDark ? styles.darkBg : styles.lightBg]}
|
||||
contentContainerStyle={{ flexGrow: 1, paddingBottom: 60 }}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
colors={['#3b82f6']}
|
||||
tintColor="#3b82f6"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Stack.Screen options={{ headerShown: false }} />
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<ScrollView
|
||||
style={[isDark ? styles.darkBg : styles.lightBg]}
|
||||
contentContainerStyle={{ flexGrow: 1 }}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
colors={['#3b82f6']}
|
||||
tintColor="#3b82f6"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Stack.Screen options={{ headerShown: false }} />
|
||||
|
||||
<View style={styles.content}>
|
||||
{/* Qidiruv va filter */}
|
||||
<View style={styles.searchSection}>
|
||||
<View
|
||||
style={[
|
||||
styles.searchInputContainer,
|
||||
isDark ? styles.darkSearchInput : styles.lightSearchInput,
|
||||
]}
|
||||
>
|
||||
<Search size={20} color={isDark ? '#64748b' : '#94a3b8'} style={styles.searchIcon} />
|
||||
<TextInput
|
||||
style={[styles.searchInput, isDark ? styles.darkInputText : styles.lightInputText]}
|
||||
placeholder={t(placeholderText)}
|
||||
value={query}
|
||||
onChangeText={setQuery}
|
||||
placeholderTextColor={isDark ? '#64748b' : '#94a3b8'}
|
||||
/>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={styles.filterButton}
|
||||
onPress={() => setShowFilter(true)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Filter size={20} color="#fff" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<SearchTabs value={activeTab} onChange={setActiveTab} />
|
||||
|
||||
{error && (
|
||||
<View style={[styles.errorContainer, isDark ? styles.darkError : styles.lightError]}>
|
||||
<Text style={styles.errorText}>{t("Ma'lumot yuklashda xatolik")}</Text>
|
||||
<TouchableOpacity onPress={onRefresh} style={styles.retryButton}>
|
||||
<Text style={styles.retryButtonText}>{t('Qayta urinish')}</Text>
|
||||
<View style={styles.content}>
|
||||
{/* Qidiruv va filter */}
|
||||
<View style={styles.searchSection}>
|
||||
<View
|
||||
style={[
|
||||
styles.searchInputContainer,
|
||||
isDark ? styles.darkSearchInput : styles.lightSearchInput,
|
||||
]}
|
||||
>
|
||||
<Search size={20} color={isDark ? '#64748b' : '#94a3b8'} style={styles.searchIcon} />
|
||||
<TextInput
|
||||
style={[styles.searchInput, isDark ? styles.darkInputText : styles.lightInputText]}
|
||||
placeholder={t(placeholderText)}
|
||||
value={query}
|
||||
onChangeText={setQuery}
|
||||
placeholderTextColor={isDark ? '#64748b' : '#94a3b8'}
|
||||
/>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={styles.filterButton}
|
||||
onPress={() => setShowFilter(true)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Filter size={20} color="#fff" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{isLoading && !refreshing && <Loading />}
|
||||
<SearchTabs value={activeTab} onChange={setActiveTab} />
|
||||
|
||||
{!isLoading && RenderedView}
|
||||
</View>
|
||||
</ScrollView>
|
||||
{error && (
|
||||
<View style={[styles.errorContainer, isDark ? styles.darkError : styles.lightError]}>
|
||||
<Text style={styles.errorText}>{t("Ma'lumot yuklashda xatolik")}</Text>
|
||||
<TouchableOpacity onPress={onRefresh} style={styles.retryButton}>
|
||||
<Text style={styles.retryButtonText}>{t('Qayta urinish')}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{isLoading && !refreshing && <Loading />}
|
||||
|
||||
{!isLoading && RenderedView}
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{/* Filter Modal */}
|
||||
|
||||
<Modal
|
||||
visible={showFilter && step === 'filter'}
|
||||
animationType="slide"
|
||||
presentationStyle="pageSheet"
|
||||
onRequestClose={handleCloseFilter}
|
||||
>
|
||||
<SafeAreaProvider>
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<FilterUI back={handleCloseFilter} setStep={setStep} setFiltered={setFiltered} />
|
||||
</SafeAreaView>
|
||||
</SafeAreaProvider>
|
||||
</Modal>
|
||||
</GestureHandlerRootView>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
MyAdsData,
|
||||
MyAdsDataRes,
|
||||
MyBonusesData,
|
||||
NotificationListRes,
|
||||
UserInfoResponseData,
|
||||
} from './type';
|
||||
|
||||
@@ -35,6 +36,8 @@ export const user_api = {
|
||||
person_type: 'employee' | 'legal_entity' | 'ytt' | 'band';
|
||||
phone: string;
|
||||
activate_types: number[];
|
||||
age: number;
|
||||
gender: 'male' | 'female';
|
||||
}) {
|
||||
const res = await httpClient.patch(API_URLS.User_Update, body);
|
||||
return res;
|
||||
@@ -105,4 +108,32 @@ export const user_api = {
|
||||
const res = await httpClient.get(API_URLS.Detail_Products(id));
|
||||
return res;
|
||||
},
|
||||
|
||||
async my_referrals(params: { page: number; page_size: number }) {
|
||||
const res = await httpClient.get(API_URLS.My_Refferals, { params });
|
||||
return res;
|
||||
},
|
||||
|
||||
async create_referral(body: {
|
||||
code: string;
|
||||
referral_share: number;
|
||||
description: string;
|
||||
is_agent: boolean;
|
||||
}) {
|
||||
const res = await httpClient.post(API_URLS.My_Refferals, body);
|
||||
return res;
|
||||
},
|
||||
|
||||
async notification_list(params: {
|
||||
page: number;
|
||||
page_size: number;
|
||||
}): Promise<AxiosResponse<NotificationListRes>> {
|
||||
const res = await httpClient.get(API_URLS.Notification_List, { params });
|
||||
return res;
|
||||
},
|
||||
|
||||
async is_ready_id(id: number) {
|
||||
const res = await httpClient.post(API_URLS.Notification_Ready(id));
|
||||
return res;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -77,6 +77,8 @@ export interface UserInfoResponseData {
|
||||
company_image: null | string;
|
||||
address: null | string;
|
||||
district: number;
|
||||
age: null | number;
|
||||
gender: 'male' | 'female' | null;
|
||||
parent: null | string;
|
||||
user_tg_ids: number[];
|
||||
};
|
||||
@@ -166,3 +168,30 @@ export interface MyBonusesDataRes {
|
||||
percent: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface NotificationListRes {
|
||||
status: boolean;
|
||||
data: {
|
||||
links: {
|
||||
previous: null | string;
|
||||
next: null | string;
|
||||
};
|
||||
total_items: number;
|
||||
total_pages: number;
|
||||
page_size: number;
|
||||
current_page: number;
|
||||
unread_count: number;
|
||||
|
||||
results: NotificationListDataRes[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface NotificationListDataRes {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
is_send: boolean;
|
||||
is_read: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
233
screens/profile/ui/CreateReferrals.tsx
Normal file
233
screens/profile/ui/CreateReferrals.tsx
Normal file
@@ -0,0 +1,233 @@
|
||||
import { useTheme } from '@/components/ThemeContext';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { ArrowLeft } from 'lucide-react-native';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Switch,
|
||||
Text,
|
||||
TextInput,
|
||||
ToastAndroid,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { user_api } from '../lib/api';
|
||||
|
||||
type FormType = {
|
||||
code: string;
|
||||
referral_share: string;
|
||||
description: string;
|
||||
is_agent: boolean;
|
||||
};
|
||||
|
||||
export default function CreateReferrals() {
|
||||
const { isDark } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const [form, setForm] = useState<FormType>({
|
||||
code: '',
|
||||
referral_share: '',
|
||||
description: '',
|
||||
is_agent: false,
|
||||
});
|
||||
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: (body: {
|
||||
code: string;
|
||||
referral_share: number;
|
||||
description: string;
|
||||
is_agent: boolean;
|
||||
}) => user_api.create_referral(body),
|
||||
onSuccess: () => {
|
||||
ToastAndroid.show(t('Referral yaratildi'), ToastAndroid.SHORT);
|
||||
queryClient.refetchQueries({ queryKey: ['my_referrals'] });
|
||||
router.back();
|
||||
},
|
||||
onError: () => {
|
||||
ToastAndroid.show(t('Xatolik yuz berdi'), ToastAndroid.SHORT);
|
||||
},
|
||||
});
|
||||
|
||||
const [errors, setErrors] = useState<any>({});
|
||||
|
||||
const update = (key: keyof FormType, value: any) => setForm((p) => ({ ...p, [key]: value }));
|
||||
|
||||
const validate = () => {
|
||||
const e: any = {};
|
||||
|
||||
if (!form.code || form.code.length !== 9)
|
||||
e.code = 'Kod aynan 9 ta belgidan iborat bo‘lishi kerak';
|
||||
if (!form.description || form.description.length < 5)
|
||||
e.description = 'Tavsif kamida 5 ta belgidan iborat bo‘lishi kerak';
|
||||
|
||||
if (form.is_agent) {
|
||||
if (!form.referral_share || Number(form.referral_share) <= 0)
|
||||
e.referral_share = 'Agent uchun foiz majburiy';
|
||||
}
|
||||
|
||||
setErrors(e);
|
||||
return Object.keys(e).length === 0;
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (!validate()) return;
|
||||
|
||||
const payload = {
|
||||
code: form.code,
|
||||
referral_share: form.is_agent ? Number(form.referral_share) : 0,
|
||||
description: form.description,
|
||||
is_agent: form.is_agent,
|
||||
};
|
||||
|
||||
mutate(payload);
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#f8fafc' }}>
|
||||
{/* HEADER */}
|
||||
<View style={[styles.header, { backgroundColor: isDark ? '#0f172a' : '#fff' }]}>
|
||||
<Pressable onPress={() => router.back()}>
|
||||
<ArrowLeft color={isDark ? '#fff' : '#0f172a'} />
|
||||
</Pressable>
|
||||
|
||||
<Text style={[styles.headerTitle, { color: isDark ? '#fff' : '#0f172a' }]}>
|
||||
{t('Referral yaratish')}
|
||||
</Text>
|
||||
|
||||
<Pressable onPress={handleSave}>
|
||||
{isPending ? (
|
||||
<ActivityIndicator size={'small'} />
|
||||
) : (
|
||||
<Text style={styles.save}>{t('Saqlash')}</Text>
|
||||
)}
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<ScrollView contentContainerStyle={styles.container}>
|
||||
{/* NOM */}
|
||||
<Text style={[styles.label, { color: isDark ? '#fff' : '#0f172a' }]}>
|
||||
{t('Referral nomi')}
|
||||
</Text>
|
||||
<View style={[styles.inputBox, theme(isDark)]}>
|
||||
<TextInput
|
||||
maxLength={9}
|
||||
style={[styles.input, { color: isDark ? '#fff' : '#0f172a' }]}
|
||||
placeholder="ABC123"
|
||||
placeholderTextColor="#94a3b8"
|
||||
value={form.code}
|
||||
onChangeText={(v) => update('code', v)}
|
||||
/>
|
||||
</View>
|
||||
{errors.code && <Text style={styles.error}>{t(errors.code)}</Text>}
|
||||
|
||||
{/* TAVSIF */}
|
||||
<Text style={[styles.label, { color: isDark ? '#fff' : '#0f172a' }]}>{t('Tavsif')}</Text>
|
||||
<View style={[styles.inputBox, styles.textArea, theme(isDark)]}>
|
||||
<TextInput
|
||||
multiline
|
||||
textAlignVertical="top"
|
||||
style={[styles.input, { color: isDark ? '#fff' : '#0f172a' }]}
|
||||
placeholder={t('Batafsil yozing...')}
|
||||
placeholderTextColor="#94a3b8"
|
||||
value={form.description}
|
||||
onChangeText={(v) => update('description', v)}
|
||||
/>
|
||||
</View>
|
||||
{errors.description && <Text style={styles.error}>{t(errors.description)}</Text>}
|
||||
|
||||
{/* AGENT SWITCH */}
|
||||
<View style={styles.switchRow}>
|
||||
<Text style={[styles.label, { color: isDark ? '#fff' : '#0f172a' }]}>
|
||||
{t('Agentmi?')}
|
||||
</Text>
|
||||
<Switch
|
||||
value={form.is_agent}
|
||||
onValueChange={(v) => {
|
||||
update('is_agent', v);
|
||||
if (!v) update('referral_share', '');
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 👉 FOIZ FAQAT AGENT YOQILGANDA */}
|
||||
{form.is_agent && (
|
||||
<>
|
||||
<Text style={[styles.label, { color: isDark ? '#fff' : '#0f172a' }]}>
|
||||
{t('Referral foizi (%)')}
|
||||
</Text>
|
||||
<View style={[styles.inputBox, theme(isDark)]}>
|
||||
<TextInput
|
||||
maxLength={1}
|
||||
keyboardType="numeric"
|
||||
style={[styles.input, { color: isDark ? '#fff' : '#0f172a' }]}
|
||||
placeholder="5"
|
||||
placeholderTextColor="#94a3b8"
|
||||
value={form.referral_share}
|
||||
onChangeText={(v) => {
|
||||
// faqat 1–5 oralig‘ini qabul qiladi
|
||||
if (v === '') {
|
||||
update('referral_share', '');
|
||||
return;
|
||||
}
|
||||
|
||||
const num = Number(v);
|
||||
if (num >= 1 && num <= 5) {
|
||||
update('referral_share', v);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
{errors.referral_share && <Text style={styles.error}>{t(errors.referral_share)}</Text>}
|
||||
</>
|
||||
)}
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const theme = (isDark: boolean) => ({
|
||||
backgroundColor: isDark ? '#1e293b' : '#fff',
|
||||
borderColor: isDark ? '#334155' : '#e2e8f0',
|
||||
});
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
header: {
|
||||
padding: 16,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
elevation: 3,
|
||||
},
|
||||
headerTitle: { fontSize: 18, fontWeight: '700' },
|
||||
save: { color: '#3b82f6', fontSize: 16, fontWeight: '600' },
|
||||
|
||||
container: { padding: 16, gap: 10 },
|
||||
label: { fontSize: 15, fontWeight: '700' },
|
||||
error: { color: '#ef4444', fontSize: 13, marginLeft: 6 },
|
||||
|
||||
inputBox: {
|
||||
flexDirection: 'row',
|
||||
borderRadius: 16,
|
||||
borderWidth: 1,
|
||||
paddingHorizontal: 6,
|
||||
height: 56,
|
||||
},
|
||||
textArea: { height: 120, alignItems: 'flex-start' },
|
||||
input: {
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
},
|
||||
|
||||
switchRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginTop: 10,
|
||||
},
|
||||
});
|
||||
190
screens/profile/ui/ManualTab.tsx
Normal file
190
screens/profile/ui/ManualTab.tsx
Normal file
@@ -0,0 +1,190 @@
|
||||
import { useTheme } from '@/components/ThemeContext';
|
||||
import { ResizeMode, Video } from 'expo-av';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { ArrowLeft } from 'lucide-react-native';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Dimensions,
|
||||
Image,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
|
||||
type ManualStep = {
|
||||
title: string;
|
||||
text: string;
|
||||
image?: any;
|
||||
};
|
||||
|
||||
export function ManualTab() {
|
||||
const { isDark } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const [videoLang, setVideoLang] = useState<'uz' | 'ru' | 'en'>('uz');
|
||||
|
||||
const theme = {
|
||||
background: isDark ? '#0f172a' : '#f8fafc',
|
||||
cardBg: isDark ? '#1e293b' : '#ffffff',
|
||||
text: isDark ? '#ffffff' : '#0f172a',
|
||||
textSecondary: isDark ? '#94a3b8' : '#64748b',
|
||||
primary: '#3b82f6',
|
||||
};
|
||||
|
||||
const steps: ManualStep[] = [
|
||||
{
|
||||
title: "Foydalanish qo'lanmasi",
|
||||
text: "Foydalanish qo'lanmasi",
|
||||
image: require('@/assets/manual/step1.jpg'),
|
||||
},
|
||||
{
|
||||
title: "Ro'yxatdan o'tish (Registratsiya) – 1 daqiqa ichida",
|
||||
text: "Platformaga kirish uchun avval ro'yxatdan o'ting.",
|
||||
image: require('@/assets/manual/step2.jpg'),
|
||||
},
|
||||
{
|
||||
title: "Profilni to'ldirish va tasdiqlash",
|
||||
text: "Muhim: Ro'yxatdan o'tgandan keyin profil to'liq bo'lishi kerak — aks holda platforma cheklangan rejimda ishlaydi.",
|
||||
image: require('@/assets/manual/step3.jpg'),
|
||||
},
|
||||
{
|
||||
title: "Xodimlarni qo'shish",
|
||||
text: "Profil ichida Xodimlar bo'limiga o'ting va + tugmasi orqali xodim qo'shing.",
|
||||
image: require('@/assets/manual/step4.jpg'),
|
||||
},
|
||||
{
|
||||
title: "E'lon berish va o'z mahsulot/xizmatlaringizni ko'rsatish",
|
||||
text: "Pastki menyudan E'lonlar bo'limiga o'ting va yangi e'lon yarating.",
|
||||
image: require('@/assets/manual/step5.jpg'),
|
||||
},
|
||||
{
|
||||
title: 'Mijozlarni qidirish va topish',
|
||||
text: 'Filtrdan foydalanib kerakli kompaniyalarni toping va profiliga kirib xabar yuboring.',
|
||||
image: require('@/assets/manual/step6.jpg'),
|
||||
},
|
||||
{
|
||||
title: 'Muhim maslahatlar va xavfsizlik',
|
||||
text: 'Faqat tasdiqlangan profillarga ishonch bilan murojaat qiling.',
|
||||
image: require('@/assets/manual/step7.jpg'),
|
||||
},
|
||||
];
|
||||
|
||||
const videos: Record<'uz' | 'ru' | 'en', any> = {
|
||||
uz: require('@/assets/manual/manual_video_uz.mp4'),
|
||||
ru: require('@/assets/manual/manual_video_ru.mp4'),
|
||||
en: require('@/assets/manual/manual_video_en.mp4'),
|
||||
};
|
||||
|
||||
const handleVideoChange = (lang: 'uz' | 'ru' | 'en') => {
|
||||
setVideoLang(lang);
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView style={[styles.container, { backgroundColor: theme.background }]}>
|
||||
<View style={[styles.header, { backgroundColor: isDark ? '#0f172a' : '#fff' }]}>
|
||||
<Pressable onPress={() => router.push('/profile')}>
|
||||
<ArrowLeft color={isDark ? '#fff' : '#0f172a'} />
|
||||
</Pressable>
|
||||
|
||||
<Text style={[styles.headerTitle, { color: isDark ? '#fff' : '#0f172a' }]}>
|
||||
{t("Foydalanish qo'lanmasi")}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{steps.map((step, index) => (
|
||||
<View key={index} style={[styles.card, { backgroundColor: theme.cardBg }]}>
|
||||
<Text style={[styles.headerTitle, { color: theme.text }]}>{t(step.title)}</Text>
|
||||
<Text style={[styles.text, { color: theme.textSecondary, marginTop: 8 }]}>
|
||||
{t(step.text)}
|
||||
</Text>
|
||||
{step.image && <Image source={step.image} style={styles.image} resizeMode="contain" />}
|
||||
</View>
|
||||
))}
|
||||
|
||||
{/* Video bo'limi */}
|
||||
<View style={[styles.card, { backgroundColor: theme.cardBg }]}>
|
||||
<Text style={[styles.headerTitle, { color: theme.text }]}>{t("Qo'llanma video")}</Text>
|
||||
|
||||
{/* Til tanlash tugmalari */}
|
||||
<View style={styles.buttonRow}>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.langButton,
|
||||
{
|
||||
backgroundColor: videoLang === 'uz' ? theme.primary : theme.cardBg,
|
||||
borderColor: theme.primary,
|
||||
},
|
||||
]}
|
||||
onPress={() => handleVideoChange('uz')}
|
||||
>
|
||||
<Text style={{ color: videoLang === 'uz' ? '#fff' : theme.text }}>O'zbek</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.langButton,
|
||||
{
|
||||
backgroundColor: videoLang === 'ru' ? theme.primary : theme.cardBg,
|
||||
borderColor: theme.primary,
|
||||
},
|
||||
]}
|
||||
onPress={() => handleVideoChange('ru')}
|
||||
>
|
||||
<Text style={{ color: videoLang === 'ru' ? '#fff' : theme.text }}>Русский</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.langButton,
|
||||
{
|
||||
backgroundColor: videoLang === 'en' ? theme.primary : theme.cardBg,
|
||||
borderColor: theme.primary,
|
||||
},
|
||||
]}
|
||||
onPress={() => handleVideoChange('en')}
|
||||
>
|
||||
<Text style={{ color: videoLang === 'en' ? '#fff' : theme.text }}>English</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<Video
|
||||
source={videos[videoLang]}
|
||||
style={styles.video}
|
||||
useNativeControls
|
||||
isLooping
|
||||
resizeMode={ResizeMode.COVER}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, padding: 16 },
|
||||
card: { borderRadius: 16, padding: 16, marginBottom: 16 },
|
||||
headerTitle: { fontSize: 18, fontWeight: '700', lineHeight: 24 },
|
||||
text: { fontSize: 14, lineHeight: 20 },
|
||||
image: { width: width - 64, height: 200, marginTop: 12, borderRadius: 12 },
|
||||
video: { width: width - 64, height: 320, marginTop: 12, borderRadius: 12 },
|
||||
buttonRow: { flexDirection: 'row', justifyContent: 'space-around', marginVertical: 12 },
|
||||
langButton: {
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 16,
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
},
|
||||
header: {
|
||||
padding: 16,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
gap: 10,
|
||||
elevation: 3,
|
||||
},
|
||||
});
|
||||
407
screens/profile/ui/NotificationTab.tsx
Normal file
407
screens/profile/ui/NotificationTab.tsx
Normal file
@@ -0,0 +1,407 @@
|
||||
import { useTheme } from '@/components/ThemeContext';
|
||||
import { useInfiniteQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { router } from 'expo-router';
|
||||
import { ArrowLeft } from 'lucide-react-native';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Animated,
|
||||
FlatList,
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { user_api } from '../lib/api';
|
||||
import { NotificationListDataRes } from '../lib/type';
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
export function NotificationTab() {
|
||||
const { isDark } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data, isLoading, isError, fetchNextPage, hasNextPage, isFetchingNextPage, refetch } =
|
||||
useInfiniteQuery({
|
||||
queryKey: ['notifications-list'],
|
||||
queryFn: async ({ pageParam = 1 }) => {
|
||||
const response = await user_api.notification_list({
|
||||
page: pageParam,
|
||||
page_size: PAGE_SIZE,
|
||||
});
|
||||
return response.data.data;
|
||||
},
|
||||
getNextPageParam: (lastPage) =>
|
||||
lastPage.current_page < lastPage.total_pages ? lastPage.current_page + 1 : undefined,
|
||||
initialPageParam: 1,
|
||||
});
|
||||
const notifications = data?.pages.flatMap((p) => p.results) ?? [];
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View style={styles.loadingContainer}>
|
||||
<View style={styles.loadingContent}>
|
||||
<ActivityIndicator size="large" color="#3b82f6" />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<View style={styles.errorContainer}>
|
||||
<View style={styles.errorContent}>
|
||||
<Text style={styles.errorTitle}>{t('Xatolik yuz berdi')}</Text>
|
||||
<Text style={styles.errorMessage}>{t("Bildirishnomalarni yuklashda muammo bo'ldi")}</Text>
|
||||
<TouchableOpacity style={styles.retryButton} onPress={() => refetch()}>
|
||||
<Text style={styles.retryButtonText}>{t('Qayta urinish')}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={[styles.header, { backgroundColor: isDark ? '#0f172a' : '#fff' }]}>
|
||||
<Pressable onPress={() => router.push('/profile')}>
|
||||
<ArrowLeft color={isDark ? '#fff' : '#0f172a'} />
|
||||
</Pressable>
|
||||
|
||||
<Text style={[styles.headerTitle, { color: isDark ? '#fff' : '#0f172a' }]}>
|
||||
{t('Bildirishnomalar')}
|
||||
</Text>
|
||||
</View>
|
||||
<FlatList
|
||||
data={notifications}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
contentContainerStyle={styles.listContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
renderItem={({ item, index }) => <NotificationCard item={item} />}
|
||||
onEndReached={() => {
|
||||
if (hasNextPage && !isFetchingNextPage) {
|
||||
fetchNextPage();
|
||||
}
|
||||
}}
|
||||
onEndReachedThreshold={0.5}
|
||||
ListFooterComponent={
|
||||
isFetchingNextPage ? (
|
||||
<ActivityIndicator size="small" color="#3b82f6" style={{ marginVertical: 20 }} />
|
||||
) : null
|
||||
}
|
||||
refreshing={isLoading}
|
||||
onRefresh={refetch}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- CARD ---------------- */
|
||||
|
||||
function NotificationCard({ item }: { item: NotificationListDataRes }) {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
const [scaleAnim] = useState(new Animated.Value(1));
|
||||
const { mutate } = useMutation({
|
||||
mutationFn: (id: number) => user_api.is_ready_id(id),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ['notification-list'] });
|
||||
},
|
||||
});
|
||||
|
||||
const handlePressIn = () => {
|
||||
Animated.spring(scaleAnim, {
|
||||
toValue: 0.96,
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
};
|
||||
|
||||
const handlePressOut = () => {
|
||||
Animated.spring(scaleAnim, {
|
||||
toValue: 1,
|
||||
friction: 4,
|
||||
tension: 50,
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
};
|
||||
|
||||
const handlePress = (id: number) => {
|
||||
if (!item.is_read) {
|
||||
mutate(id);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={{
|
||||
transform: [{ scale: scaleAnim }],
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.85}
|
||||
onPressIn={handlePressIn}
|
||||
onPressOut={handlePressOut}
|
||||
onPress={() => handlePress(item.id)}
|
||||
style={[styles.card, !item.is_read && styles.unreadCard]}
|
||||
>
|
||||
<View style={styles.cardContent}>
|
||||
<View style={styles.cardHeader}>
|
||||
<Text style={[styles.cardTitle, !item.is_read && styles.unreadTitle]} numberOfLines={1}>
|
||||
{item.title}
|
||||
</Text>
|
||||
{!item.is_read && <View style={styles.unreadIndicator} />}
|
||||
</View>
|
||||
|
||||
<Text style={styles.cardMessage} numberOfLines={2}>
|
||||
{item.description}
|
||||
</Text>
|
||||
|
||||
<Text style={styles.cardTime}>{formatDate(item.created_at, t)}</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- HELPERS ---------------- */
|
||||
|
||||
function formatDate(date: string, t: any) {
|
||||
const now = new Date();
|
||||
const notifDate = new Date(date);
|
||||
const diffMs = now.getTime() - notifDate.getTime();
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMs / 3600000);
|
||||
const diffDays = Math.floor(diffMs / 86400000);
|
||||
|
||||
if (diffMins < 1) return 'Hozir';
|
||||
if (diffMins < 60) return `${diffMins} ${t('daqiqa oldin')}`;
|
||||
if (diffHours < 24) return `${diffHours} ${t('soat oldin')}`;
|
||||
if (diffDays < 7) return `${diffDays} ${t('kun oldin')}`;
|
||||
|
||||
return notifDate.toLocaleDateString('uz-UZ', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------- STYLES ---------------- */
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#0a0c17',
|
||||
},
|
||||
listContent: {
|
||||
padding: 16,
|
||||
paddingBottom: 32,
|
||||
},
|
||||
|
||||
/* Card Styles */
|
||||
card: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: '#121826',
|
||||
padding: 16,
|
||||
borderRadius: 24,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(60, 70, 90, 0.18)',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 8 },
|
||||
shadowOpacity: 0.35,
|
||||
shadowRadius: 16,
|
||||
elevation: 10,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
unreadCard: {
|
||||
backgroundColor: '#1a2236',
|
||||
borderColor: 'rgba(59, 130, 246, 0.4)',
|
||||
shadowColor: '#3b82f6',
|
||||
shadowOpacity: 0.28,
|
||||
shadowRadius: 20,
|
||||
elevation: 12,
|
||||
},
|
||||
iconContainer: {
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 20,
|
||||
backgroundColor: 'rgba(30, 38, 56, 0.7)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: 16,
|
||||
},
|
||||
unreadIconContainer: {
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.22)',
|
||||
},
|
||||
iconText: {
|
||||
fontSize: 32,
|
||||
},
|
||||
cardContent: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
cardHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 6,
|
||||
},
|
||||
cardTitle: {
|
||||
flex: 1,
|
||||
color: '#d1d5db',
|
||||
fontSize: 16.5,
|
||||
fontWeight: '600',
|
||||
letterSpacing: -0.1,
|
||||
},
|
||||
unreadTitle: {
|
||||
color: '#f1f5f9',
|
||||
fontWeight: '700',
|
||||
},
|
||||
unreadIndicator: {
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: 5,
|
||||
backgroundColor: '#3b82f6',
|
||||
marginLeft: 8,
|
||||
shadowColor: '#3b82f6',
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0.7,
|
||||
shadowRadius: 6,
|
||||
},
|
||||
cardMessage: {
|
||||
color: '#9ca3af',
|
||||
fontSize: 14.5,
|
||||
lineHeight: 21,
|
||||
marginBottom: 8,
|
||||
},
|
||||
cardTime: {
|
||||
color: '#64748b',
|
||||
fontSize: 12.5,
|
||||
fontWeight: '500',
|
||||
opacity: 0.9,
|
||||
},
|
||||
|
||||
/* Loading State */
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: '#0a0c17',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
loadingContent: {
|
||||
alignItems: 'center',
|
||||
padding: 32,
|
||||
},
|
||||
loadingText: {
|
||||
marginTop: 16,
|
||||
color: '#94a3b8',
|
||||
fontSize: 15,
|
||||
fontWeight: '500',
|
||||
},
|
||||
|
||||
/* Error State */
|
||||
errorContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: '#0a0c17',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 24,
|
||||
},
|
||||
errorContent: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#151b2e',
|
||||
padding: 32,
|
||||
borderRadius: 24,
|
||||
maxWidth: 320,
|
||||
},
|
||||
errorIconContainer: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 40,
|
||||
backgroundColor: '#1e2638',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 20,
|
||||
},
|
||||
errorIcon: {
|
||||
fontSize: 40,
|
||||
},
|
||||
errorTitle: {
|
||||
color: '#ef4444',
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
marginBottom: 8,
|
||||
},
|
||||
errorMessage: {
|
||||
color: '#94a3b8',
|
||||
fontSize: 14,
|
||||
textAlign: 'center',
|
||||
marginBottom: 24,
|
||||
lineHeight: 20,
|
||||
},
|
||||
retryButton: {
|
||||
backgroundColor: '#3b82f6',
|
||||
paddingHorizontal: 32,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 12,
|
||||
shadowColor: '#3b82f6',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 8,
|
||||
elevation: 5,
|
||||
},
|
||||
retryButtonText: {
|
||||
color: '#ffffff',
|
||||
fontSize: 15,
|
||||
fontWeight: '700',
|
||||
},
|
||||
|
||||
/* Empty State */
|
||||
emptyContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: '#0a0c17',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 24,
|
||||
},
|
||||
emptyContent: {
|
||||
alignItems: 'center',
|
||||
maxWidth: 300,
|
||||
},
|
||||
emptyIconContainer: {
|
||||
width: 100,
|
||||
height: 100,
|
||||
borderRadius: 50,
|
||||
backgroundColor: '#151b2e',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 24,
|
||||
borderWidth: 2,
|
||||
borderColor: '#1e2638',
|
||||
},
|
||||
emptyIcon: {
|
||||
fontSize: 50,
|
||||
},
|
||||
emptyTitle: {
|
||||
color: '#ffffff',
|
||||
fontSize: 22,
|
||||
fontWeight: '700',
|
||||
marginBottom: 12,
|
||||
textAlign: 'center',
|
||||
},
|
||||
emptyMessage: {
|
||||
color: '#94a3b8',
|
||||
fontSize: 15,
|
||||
textAlign: 'center',
|
||||
lineHeight: 22,
|
||||
},
|
||||
header: {
|
||||
padding: 16,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
gap: 10,
|
||||
elevation: 3,
|
||||
},
|
||||
headerTitle: { fontSize: 18, fontWeight: '700', lineHeight: 24 },
|
||||
});
|
||||
@@ -4,11 +4,9 @@ import { Edit2, Plus } from 'lucide-react-native';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Alert, Modal, Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
|
||||
import { user_api } from '../lib/api';
|
||||
import { useProfileData } from '../lib/ProfileDataContext';
|
||||
import { UserInfoResponseData } from '../lib/type';
|
||||
|
||||
export function PersonalInfoTab() {
|
||||
const { personalInfo, updatePersonalInfo } = useProfileData();
|
||||
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||
const [addFieldModalVisible, setAddFieldModalVisible] = useState(false);
|
||||
const [newField, setNewField] = useState('');
|
||||
@@ -27,7 +25,6 @@ export function PersonalInfoTab() {
|
||||
queryKey: ['get_me'],
|
||||
queryFn: () => user_api.getMe(),
|
||||
select: (res) => {
|
||||
setEditData(res.data.data);
|
||||
setPhone(res.data.data.phone || '');
|
||||
return res;
|
||||
},
|
||||
@@ -51,8 +48,11 @@ export function PersonalInfoTab() {
|
||||
code: string;
|
||||
};
|
||||
}[];
|
||||
phone: string;
|
||||
person_type: 'employee' | 'legal_entity' | 'ytt' | 'band';
|
||||
phone: string;
|
||||
activate_types: number[];
|
||||
age: number;
|
||||
gender: 'male' | 'female';
|
||||
}) => user_api.updateMe(body),
|
||||
onSuccess: (res) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['get_me'] });
|
||||
@@ -102,9 +102,6 @@ export function PersonalInfoTab() {
|
||||
|
||||
const handleAddField = () => {
|
||||
if (newField.trim()) {
|
||||
updatePersonalInfo({
|
||||
activityFields: [...personalInfo.activityFields, newField.trim()],
|
||||
});
|
||||
setNewField('');
|
||||
setAddFieldModalVisible(false);
|
||||
}
|
||||
@@ -116,18 +113,12 @@ export function PersonalInfoTab() {
|
||||
{
|
||||
text: 'Olib tashlash',
|
||||
style: 'destructive',
|
||||
onPress: () => {
|
||||
updatePersonalInfo({
|
||||
activityFields: personalInfo.activityFields.filter((f) => f !== field),
|
||||
});
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (me?.data.data) {
|
||||
setEditData(me.data.data);
|
||||
setPhone(me.data.data.phone || '');
|
||||
}
|
||||
}, [me]);
|
||||
@@ -201,7 +192,7 @@ export function PersonalInfoTab() {
|
||||
<Text style={styles.inputLabel}>Ism</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={editData?.director_full_name}
|
||||
value={editData?.data.first_name}
|
||||
onChangeText={(text) =>
|
||||
setEditData((prev) => prev && { ...prev, director_full_name: text })
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { useTheme } from '@/components/ThemeContext';
|
||||
import { useGlobalRefresh } from '@/components/ui/RefreshContext';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useRouter } from 'expo-router';
|
||||
import {
|
||||
Award,
|
||||
Bell,
|
||||
BookAIcon,
|
||||
ChevronRight,
|
||||
HandCoins,
|
||||
Megaphone,
|
||||
Package,
|
||||
Settings,
|
||||
@@ -13,6 +17,7 @@ import {
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { RefreshControl } from 'react-native-gesture-handler';
|
||||
import { user_api } from '../lib/api';
|
||||
|
||||
export default function Profile() {
|
||||
const router = useRouter();
|
||||
@@ -20,12 +25,18 @@ export default function Profile() {
|
||||
const { isDark } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data: me, isLoading } = useQuery({
|
||||
queryKey: ['get_me'],
|
||||
queryFn: () => user_api.getMe(),
|
||||
});
|
||||
|
||||
const sections = [
|
||||
{
|
||||
title: 'Shaxsiy',
|
||||
items: [
|
||||
{ icon: User, label: "Shaxsiy ma'lumotlar", route: '/profile/personal-info' },
|
||||
{ icon: Users, label: 'Xodimlar', route: '/profile/employees' },
|
||||
{ icon: Bell, label: 'Bildirishnomalar', route: '/profile/notification' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -34,11 +45,23 @@ export default function Profile() {
|
||||
{ icon: Megaphone, label: "E'lonlar", route: '/profile/my-ads' },
|
||||
{ icon: Award, label: 'Bonuslar', route: '/profile/bonuses' },
|
||||
{ icon: Package, label: 'Xizmatlar', route: '/profile/products' },
|
||||
...(me?.data.data.can_create_referral
|
||||
? [
|
||||
{
|
||||
icon: HandCoins,
|
||||
label: 'Refferallarim',
|
||||
route: '/profile/my-referrals',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Sozlamalar',
|
||||
items: [{ icon: Settings, label: 'Sozlamalar', route: '/profile/settings' }],
|
||||
items: [
|
||||
{ icon: Settings, label: 'Sozlamalar', route: '/profile/settings' },
|
||||
{ icon: BookAIcon, label: "Foydalanish qo'lanmasi", route: '/profile/manual' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
203
screens/profile/ui/RefferallsTab.tsx
Normal file
203
screens/profile/ui/RefferallsTab.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
import { useTheme } from '@/components/ThemeContext';
|
||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import * as Clipboard from 'expo-clipboard';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { ArrowLeft, CopyIcon, HandCoins, Plus, Users } from 'lucide-react-native';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
FlatList,
|
||||
Pressable,
|
||||
RefreshControl,
|
||||
StyleSheet,
|
||||
Text,
|
||||
ToastAndroid,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { user_api } from '../lib/api';
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
export function ReferralsTab() {
|
||||
const router = useRouter();
|
||||
const { isDark } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const theme = {
|
||||
background: isDark ? '#0f172a' : '#f8fafc',
|
||||
cardBg: isDark ? '#1e293b' : '#ffffff',
|
||||
text: isDark ? '#ffffff' : '#0f172a',
|
||||
subText: isDark ? '#94a3b8' : '#64748b',
|
||||
primary: '#3b82f6',
|
||||
success: '#10b981',
|
||||
};
|
||||
|
||||
const { data, isLoading, isError, fetchNextPage, hasNextPage, refetch } = useInfiniteQuery({
|
||||
queryKey: ['my_referrals'],
|
||||
queryFn: async ({ pageParam = 1 }) => {
|
||||
const res = await user_api.my_referrals({
|
||||
page: pageParam,
|
||||
page_size: PAGE_SIZE,
|
||||
});
|
||||
|
||||
const d = res.data.data;
|
||||
return {
|
||||
results: d.results ?? [],
|
||||
current_page: d.current_page,
|
||||
total_pages: d.total_pages,
|
||||
};
|
||||
},
|
||||
getNextPageParam: (lastPage) =>
|
||||
lastPage.current_page < lastPage.total_pages ? lastPage.current_page + 1 : undefined,
|
||||
initialPageParam: 1,
|
||||
});
|
||||
|
||||
const referrals = data?.pages.flatMap((p) => p.results) ?? [];
|
||||
|
||||
const onRefresh = async () => {
|
||||
setRefreshing(true);
|
||||
await refetch();
|
||||
setRefreshing(false);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<ActivityIndicator size="large" />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<View style={[styles.center, { backgroundColor: theme.background }]}>
|
||||
<Text style={{ color: 'red' }}>{t('Xatolik yuz berdi')}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: theme.background }]}>
|
||||
{/* Header */}
|
||||
<View style={styles.topHeader}>
|
||||
<Pressable onPress={() => router.push('/profile')}>
|
||||
<ArrowLeft color={theme.text} />
|
||||
</Pressable>
|
||||
<Text style={[styles.headerTitle, { color: theme.text }]}>{t('Refferallarim')}</Text>
|
||||
<Pressable onPress={() => router.push('/profile/added-referalls')}>
|
||||
<Plus color={theme.primary} />
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<FlatList
|
||||
data={referrals}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
contentContainerStyle={styles.list}
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={theme.primary} />
|
||||
}
|
||||
onEndReached={() => hasNextPage && fetchNextPage()}
|
||||
renderItem={({ item }) => (
|
||||
<View style={[styles.card, { backgroundColor: theme.cardBg }]}>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<View style={styles.cardHeader}>
|
||||
<HandCoins size={20} color={theme.primary} />
|
||||
<Text style={[styles.code, { color: theme.text }]}>{item.code}</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={async () => {
|
||||
await Clipboard.setStringAsync(
|
||||
`https://t.me/infotargetbot/join?startapp=${item.code}`
|
||||
);
|
||||
ToastAndroid.show('Refferal kopiya qilindi', ToastAndroid.SHORT);
|
||||
}}
|
||||
>
|
||||
<CopyIcon size={20} color={theme.primary} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<Text style={{ color: theme.subText }}>{item.description}</Text>
|
||||
|
||||
<View style={styles.footer}>
|
||||
<View style={styles.row}>
|
||||
<Users size={16} color={theme.subText} />
|
||||
<Text style={[styles.meta, { color: theme.subText }]}>
|
||||
{item.referral_registered_count} {t('foydalanuvchi')}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Text style={[styles.amount, { color: theme.success }]}>
|
||||
{item.referral_income_amount} {t("so'm")}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
ListEmptyComponent={
|
||||
<Text style={{ textAlign: 'center', color: theme.subText }}>
|
||||
{t('Refferallar topilmadi')}
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1 },
|
||||
center: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
|
||||
topHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
padding: 16,
|
||||
alignItems: 'center',
|
||||
},
|
||||
headerTitle: { fontSize: 18, fontWeight: '700' },
|
||||
|
||||
list: { padding: 16, gap: 12 },
|
||||
|
||||
card: {
|
||||
borderRadius: 16,
|
||||
padding: 16,
|
||||
gap: 10,
|
||||
},
|
||||
|
||||
cardHeader: {
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
alignItems: 'center',
|
||||
},
|
||||
|
||||
code: {
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
},
|
||||
|
||||
footer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginTop: 6,
|
||||
},
|
||||
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
gap: 6,
|
||||
alignItems: 'center',
|
||||
},
|
||||
|
||||
meta: {},
|
||||
amount: {
|
||||
fontWeight: '700',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user