complated project

This commit is contained in:
Samandar Turgunboyev
2026-02-02 18:51:53 +05:00
parent f0183e4573
commit a7419929f8
57 changed files with 3035 additions and 477 deletions

View 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 bolishi kerak';
if (!form.description || form.description.length < 5)
e.description = 'Tavsif kamida 5 ta belgidan iborat bolishi 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 15 oraligini 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,
},
});

View 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,
},
});

View 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 },
});

View File

@@ -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 })
}

View File

@@ -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' },
],
},
];

View 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',
},
});