complated

This commit is contained in:
Samandar Turgunboyev
2026-02-17 10:46:57 +05:00
parent 754f11804a
commit d747c72c8d
71 changed files with 917 additions and 397 deletions

View File

@@ -18,16 +18,15 @@ export interface AnnouncementListBodyRes {
title: string;
description: string;
total_view_count: number;
files: [
{
file: string;
}
];
files: {
id: number;
file: string;
}[];
status: 'pending' | 'paid' | 'verified' | 'canceled';
types: {
id: number;
name: string;
icon_name: string;
icon_name: string | null;
}[];
created_at: string;
}

View File

@@ -1,31 +1,86 @@
import { useTheme } from '@/components/ThemeContext';
import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query';
import { useVideoPlayer, VideoPlayer, VideoView } from 'expo-video';
import { Play } from 'lucide-react-native';
import React, { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ActivityIndicator, Animated, RefreshControl, StyleSheet, Text, View } from 'react-native';
import {
ActivityIndicator,
Animated,
RefreshControl,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { announcement_api } from '../lib/api';
import { AnnouncementListBodyRes } from '../lib/type';
import AnnouncementCard from './AnnouncementCard';
import EmptyState from './EmptyState';
function VideoCard({ player }: { player: VideoPlayer }) {
const [isPlaying, setIsPlaying] = useState(false);
useEffect(() => {
const subscription = player.addListener('playingChange', (state) => {
setIsPlaying(state.isPlaying);
});
return () => {
subscription.remove();
};
}, [player]);
return (
<View>
<VideoView player={player} style={styles.video} contentFit="contain" />
{!isPlaying && (
<View style={styles.playOverlay}>
<TouchableOpacity
style={styles.playButton}
onPress={() => {
player.play();
setIsPlaying(true);
}}
>
<Play color="white" size={26} fill="black" />
</TouchableOpacity>
</View>
)}
</View>
);
}
export default function DashboardScreen() {
const [announcements, setAnnouncements] = useState<AnnouncementListBodyRes[]>([]);
const queryClient = useQueryClient();
const fadeAnim = useRef(new Animated.Value(0)).current;
const { isDark } = useTheme();
const { t } = useTranslation();
const { t, i18n } = useTranslation();
const userLang = i18n.language.startsWith('ru')
? 'ru'
: i18n.language.startsWith('en')
? 'en'
: 'uz';
const [selectedLang, setSelectedLang] = useState<'uz' | 'ru' | 'en'>(userLang);
const theme = {
background: isDark ? '#0f172a' : '#f8fafc',
primary: '#2563eb',
text: isDark ? '#f8fafc' : '#0f172a',
loaderBg: isDark ? '#0f172a' : '#ffffff',
};
// Announcements query
const { data, isLoading, isRefetching, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey: ['announcements_list'],
queryFn: async ({ pageParam = 1 }) => {
const res = await announcement_api.list({ page: pageParam, page_size: 10 });
const res = await announcement_api.list({
page: pageParam,
page_size: 10,
});
return res.data.data;
},
getNextPageParam: (lastPage) =>
@@ -36,8 +91,6 @@ export default function DashboardScreen() {
const allAnnouncements = data?.pages.flatMap((p) => p.results) ?? [];
useEffect(() => {
setAnnouncements(allAnnouncements);
fadeAnim.setValue(0);
Animated.timing(fadeAnim, {
toValue: 1,
@@ -46,6 +99,44 @@ export default function DashboardScreen() {
}).start();
}, [allAnnouncements]);
// Announcement videos
const videos = {
uz: require('@/assets/announcements-video/video_uz.webm'),
ru: require('@/assets/announcements-video/video_ru.webm'),
en: require('@/assets/announcements-video/video_en.webm'),
};
// Government videos: faqat RU mavjud
const govermentVideos: Partial<Record<'uz' | 'ru' | 'en', any>> = {
ru: require('@/assets/goverment/video_ru.webm'),
};
// Update selected language
useEffect(() => {
const lang = i18n.language.startsWith('ru')
? 'ru'
: i18n.language.startsWith('en')
? 'en'
: 'uz';
setSelectedLang(lang);
}, [i18n.language]);
// 🔹 Hooks: conditional emas, har doim chaqiriladi
const player = useVideoPlayer(videos[selectedLang], (player) => {
player.loop = false;
player.volume = 1;
player.muted = false;
});
const govermentVideoSource = govermentVideos[selectedLang] ?? null;
const player2 = useVideoPlayer(govermentVideoSource, (player) => {
if (!govermentVideoSource) return; // no video, do nothing
player.loop = false;
player.volume = 1;
player.muted = false;
});
const onRefresh = () => {
queryClient.refetchQueries({ queryKey: ['announcements_list'] });
};
@@ -54,6 +145,21 @@ export default function DashboardScreen() {
if (hasNextPage) fetchNextPage();
};
const videoItems = [
{ id: '1', player },
govermentVideoSource && { id: '2', player: player2 },
].filter(Boolean) as { id: string; player: VideoPlayer }[];
const renderVideoHeader = () => (
<View style={{ marginBottom: 8 }}>
{videoItems.map((item) => (
<View key={item.id} style={styles.videoContainer}>
<VideoCard player={item.player} />
</View>
))}
</View>
);
if (isLoading) {
return (
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
@@ -66,33 +172,33 @@ export default function DashboardScreen() {
return (
<View style={[styles.container, { backgroundColor: theme.background }]}>
<Text style={{ color: 'white', fontSize: 20, marginBottom: 10 }}>
<Text style={{ color: theme.text, fontSize: 20, marginVertical: 16 }}>
{t("E'lonlar ro'yxati")}
</Text>
{announcements.length > 0 ? (
<Animated.FlatList
style={{ flex: 1 }}
data={announcements}
keyExtractor={(item) => item.id.toString()}
numColumns={2}
columnWrapperStyle={styles.columnWrapper}
renderItem={({ item }) => <AnnouncementCard announcement={item} />}
refreshControl={
<RefreshControl
refreshing={isRefetching}
onRefresh={onRefresh}
colors={[theme.primary]}
tintColor={theme.primary}
progressBackgroundColor={theme.background}
/>
}
onEndReached={loadMore}
onEndReachedThreshold={0.3}
showsVerticalScrollIndicator={false}
/>
) : (
<EmptyState onRefresh={onRefresh} isRefreshing={isRefetching} />
)}
<Animated.FlatList
style={{ flex: 1, opacity: fadeAnim }}
data={allAnnouncements}
keyExtractor={(item) => item.id.toString()}
numColumns={2}
columnWrapperStyle={styles.columnWrapper}
renderItem={({ item }) => <AnnouncementCard announcement={item} />}
ListHeaderComponent={renderVideoHeader}
refreshControl={
<RefreshControl
refreshing={isRefetching}
onRefresh={onRefresh}
colors={[theme.primary]}
tintColor={theme.primary}
progressBackgroundColor={theme.background}
/>
}
ListEmptyComponent={<EmptyState onRefresh={onRefresh} isRefreshing={isRefetching} />}
onEndReached={loadMore}
onEndReachedThreshold={0.3}
contentContainerStyle={{ paddingBottom: 80 }}
showsVerticalScrollIndicator={false}
/>
</View>
);
}
@@ -101,7 +207,6 @@ const styles = StyleSheet.create({
container: {
flex: 1,
paddingHorizontal: 16,
marginTop: 20,
},
loaderBox: {
flex: 1,
@@ -112,4 +217,33 @@ const styles = StyleSheet.create({
justifyContent: 'space-between',
gap: 12,
},
videoContainer: {
width: '100%',
height: 250,
marginBottom: 8,
borderRadius: 12,
overflow: 'hidden',
backgroundColor: '#000',
},
video: {
width: '100%',
height: '100%',
},
playOverlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
justifyContent: 'center',
alignItems: 'center',
},
playButton: {
backgroundColor: 'white',
borderRadius: 50,
width: 48,
height: 48,
justifyContent: 'center',
alignItems: 'center',
},
});

View File

@@ -2,6 +2,7 @@ import { useTheme } from '@/components/ThemeContext';
import { Ionicons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { ActivityIndicator, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
type Props = {
@@ -18,7 +19,7 @@ export default function EmptyState({
isRefreshing = false,
}: Props) {
const { isDark } = useTheme();
const { t } = useTranslation();
const theme = {
gradientColors: isDark
? (['#1e293b', '#334155'] as [string, string])
@@ -37,8 +38,8 @@ export default function EmptyState({
<Ionicons name="megaphone-outline" size={64} color={theme.iconColor} />
</LinearGradient>
<Text style={[emptyStyles.title, { color: theme.title }]}>{title}</Text>
<Text style={[emptyStyles.description, { color: theme.description }]}>{description}</Text>
<Text style={[emptyStyles.title, { color: theme.title }]}>{t(title)}</Text>
<Text style={[emptyStyles.description, { color: theme.description }]}>{t(description)}</Text>
{onRefresh && (
<TouchableOpacity
@@ -52,7 +53,9 @@ export default function EmptyState({
) : (
<>
<Ionicons name="refresh" size={20} color={theme.buttonText} />
<Text style={[emptyStyles.refreshText, { color: theme.buttonText }]}>Yangilash</Text>
<Text style={[emptyStyles.refreshText, { color: theme.buttonText }]}>
{t('Yangilash')}
</Text>
</>
)}
</TouchableOpacity>

View File

@@ -1,7 +1,9 @@
import { useAuth } from '@/components/AuthProvider';
import { registerForPushNotificationsAsync } from '@/components/NotificationProvider';
import AuthHeader from '@/components/ui/AuthHeader';
import { commonRequests } from '@/hooks/useNotifications';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useMutation } from '@tanstack/react-query';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import * as Haptics from 'expo-haptics';
import { LinearGradient } from 'expo-linear-gradient';
import { Redirect, useRouter } from 'expo-router';
@@ -26,6 +28,7 @@ import ConfirmForm from './ConfirmForm';
const ConfirmScreen = () => {
const router = useRouter();
const queryClient = useQueryClient();
const [phoneOTP, setPhone] = useState<string | null>('');
const [error, setError] = useState<string>('');
const { login } = useAuth();
@@ -59,11 +62,27 @@ const ConfirmScreen = () => {
const { mutate, isPending } = useMutation({
mutationFn: (body: { code: string; phone: string }) => auth_api.verify_otp(body),
onSuccess: async (res) => {
// Tokenlarni saqlash
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);
await login(res.data.data.token.access);
// **Push tokenni qayta serverga yuborish**
const pushToken = await registerForPushNotificationsAsync();
if (pushToken) {
await commonRequests.registerDevice({
token: pushToken,
platform: Platform.OS,
});
}
// Notification querylarni refetch
queryClient.refetchQueries({ queryKey: ['notification-list'] });
queryClient.refetchQueries({ queryKey: ['notifications-list'] });
// Dashboardga yonaltirish
router.replace('/(dashboard)');
},
onError: (err: any) => {

View File

@@ -1,7 +1,9 @@
import { useAuth } from '@/components/AuthProvider';
import { registerForPushNotificationsAsync } from '@/components/NotificationProvider';
import AuthHeader from '@/components/ui/AuthHeader';
import { commonRequests } from '@/hooks/useNotifications';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useMutation } from '@tanstack/react-query';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import * as Haptics from 'expo-haptics';
import { LinearGradient } from 'expo-linear-gradient';
import { Redirect, useRouter } from 'expo-router';
@@ -29,6 +31,7 @@ const RegisterConfirmScreen = () => {
const [phoneOTP, setPhone] = useState<string | null>('');
const [error, setError] = useState<string>('');
const { login } = useAuth();
const queryClient = useQueryClient();
const { savedToken } = useTokenStore();
const [resendTimer, setResendTimer] = useState<number>(60);
@@ -64,6 +67,17 @@ const RegisterConfirmScreen = () => {
savedToken(res.data.data.token.access);
await AsyncStorage.setItem('refresh_token', res.data.data.token.refresh);
await login(res.data.data.token.access);
const pushToken = await registerForPushNotificationsAsync();
if (pushToken) {
await commonRequests.registerDevice({
token: pushToken,
platform: Platform.OS,
});
}
// Notification querylarni refetch
queryClient.refetchQueries({ queryKey: ['notification-list'] });
queryClient.refetchQueries({ queryKey: ['notifications-list'] });
router.replace('/(dashboard)');
},
onError: (err: any) => {

View File

@@ -5,6 +5,7 @@ import {
BottomSheetModal,
BottomSheetScrollView,
} from '@gorhom/bottom-sheet';
import { Image } from 'expo-image';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
@@ -12,6 +13,7 @@ import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
export type Option = {
label: string;
value: string;
flag?: string;
};
type CategorySelectorProps = {
@@ -91,6 +93,12 @@ export default function CategorySelectorBottomSheet({
onClose();
}}
>
{item.flag ? (
<Image
source={{ uri: `https://flagcdn.com/24x18/${item.flag.toLowerCase()}.png` }}
style={{ width: 24, height: 18, borderRadius: 2 }}
/>
) : null}
<Text
style={[
styles.optionText,
@@ -129,6 +137,10 @@ const styles = StyleSheet.create({
paddingVertical: 16,
paddingHorizontal: 20,
borderBottomWidth: 1,
flexDirection: 'row',
alignContent: 'center',
alignItems: 'center',
gap: 10,
},
optionText: {
fontSize: 16,

View File

@@ -16,6 +16,7 @@ import {
View,
} from 'react-native';
import OneClick from '@/assets/images/one_click.png';
import PAYME from '@/assets/images/Payme_NEW.png';
import { useTranslation } from 'react-i18next';
import { price_calculation } from '../lib/api';
@@ -210,14 +211,13 @@ export default function CreateAdsScreens() {
style={[styles.safeArea, isDark ? styles.darkBg : styles.lightBg]}
>
<ScrollView contentContainerStyle={[styles.container, { paddingBottom: 90 }]}>
<Image
source={OneClick}
style={{ width: 180, height: 56, marginBottom: 10 }}
resizeMode="contain"
/>
<Text style={[styles.title, isDark ? styles.darkText : styles.lightText]}>
{currentStep === 1
? t("E'lon ma'lumotlari")
: currentStep === 2
? t('Sohalar')
: currentStep === 3
? t('Manzil')
: t("To'lov")}
{t("Bir Zumda Jonatish")}
</Text>
{currentStep === 1 && (
@@ -334,7 +334,7 @@ const styles = StyleSheet.create({
backgroundColor: '#f8fafc',
},
container: { padding: 20 },
title: { fontSize: 22, fontWeight: '800', marginBottom: 20 },
title: { fontSize: 18, fontWeight: '800', marginBottom: 20 },
darkText: {
color: '#f1f5f9',
},

View File

@@ -6,7 +6,6 @@ import React, { forwardRef, useCallback, useImperativeHandle, useState } from 'r
import { useTranslation } from 'react-i18next';
import { Image, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native';
type MediaType = { uri: string; type: 'image' | 'video' };
type StepProps = { formData: any; updateForm: (key: string, value: any) => void };
type Errors = {
@@ -302,7 +301,7 @@ const styles = StyleSheet.create({
paddingHorizontal: 16,
height: 56,
},
textArea: { height: 120, alignItems: 'flex-start', paddingVertical: 12 },
textArea: { height: 120, alignItems: 'flex-start', paddingVertical: 0 },
input: { flex: 1, fontSize: 16 },
prefixContainer: {
flexDirection: 'row',

View File

@@ -76,6 +76,17 @@ const StepThree = forwardRef(({ formData, updateForm, data }: StepProps, ref) =>
}
};
const toggleSelectAllCompanies = () => {
const selected = formData.company || [];
if (selected.length === corporations.length) {
// Deselect all
updateForm('company', []);
} else {
// Select all
updateForm('company', corporations);
}
};
useEffect(() => {
const country = statesData?.find((c) => c.code === formData.country);
setRegions(country?.region || []);
@@ -90,13 +101,19 @@ const StepThree = forwardRef(({ formData, updateForm, data }: StepProps, ref) =>
const country = statesData?.find((c) => c.code === formData.country);
const region = country?.region.find((r) => r.code === formData.region);
setDistricts(region?.districts || []);
if (!region?.districts.some((d) => d.code === formData.district)) {
// If region is 'all', automatically set district to 'all'
if (formData.region === 'all') {
updateForm('district', 'all');
} else if (!region?.districts.some((d) => d.code === formData.district)) {
updateForm('district', '');
}
}, [formData.region, formData.country, statesData]);
const getLabel = (arr: { name: string; code: string }[], val: string) =>
arr.find((item) => item.code === val)?.name || t('— Tanlang —');
const getLabel = (arr: { name: string; code: string }[], val: string) => {
if (val === 'all') return t('Hammasi');
return arr.find((item) => item.code === val)?.name || t('— Tanlang —');
};
const renderCompanyItem = ({ item }: ListRenderItemInfo<{ id: number; latter: string }>) => {
const isSelected = formData.company?.some((c: any) => c.id === item.id);
@@ -157,7 +174,13 @@ const StepThree = forwardRef(({ formData, updateForm, data }: StepProps, ref) =>
<TouchableOpacity
style={[
styles.pickerButton,
{ backgroundColor: theme.cardBg, borderColor: theme.cardBorder },
{
backgroundColor: theme.cardBg,
borderColor: theme.cardBorder,
flexDirection: 'row',
alignItems: 'center',
gap: 10,
},
]}
onPress={() => setShowCountry(true)}
>
@@ -190,9 +213,18 @@ const StepThree = forwardRef(({ formData, updateForm, data }: StepProps, ref) =>
<TouchableOpacity
style={[
styles.pickerButton,
{ backgroundColor: theme.cardBg, borderColor: theme.cardBorder },
{
backgroundColor: theme.cardBg,
borderColor: theme.cardBorder,
opacity: formData.region === 'all' ? 0.5 : 1,
},
]}
onPress={() => setShowDistrict(true)}
onPress={() => {
if (formData.region !== 'all') {
setShowDistrict(true);
}
}}
disabled={formData.region === 'all'}
>
<Text style={[styles.pickerText, { color: theme.text }]}>
{getLabel(
@@ -202,9 +234,34 @@ const StepThree = forwardRef(({ formData, updateForm, data }: StepProps, ref) =>
</Text>
</TouchableOpacity>
<Text style={[styles.sectionTitle, { color: theme.text }]}>
{t('Reklama joylashtirish kompaniyasi')}
</Text>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<Text style={[styles.sectionTitle, { color: theme.text }]}>
{t('Reklama joylashtirish kompaniyasi')}
</Text>
<TouchableOpacity
style={[
styles.selectAllButton,
{
backgroundColor:
formData.company?.length === corporations.length ? theme.primary : theme.cardBg,
borderColor: theme.primary,
},
]}
onPress={toggleSelectAllCompanies}
>
<Text
style={[
styles.selectAllText,
{
color:
formData.company?.length === corporations.length ? '#fff' : theme.primary,
},
]}
>
{formData.company?.length === corporations.length ? t('Bekor qilish') : t('Hammasi')}
</Text>
</TouchableOpacity>
</View>
<FlatList
data={corporations}
renderItem={renderCompanyItem}
@@ -232,21 +289,29 @@ const StepThree = forwardRef(({ formData, updateForm, data }: StepProps, ref) =>
isOpen={showCountry}
onClose={() => setShowCountry(false)}
selectedValue={formData.country}
data={statesData ? statesData.map((c) => ({ label: c.name, value: c.code })) : []}
data={
statesData ? statesData.map((c) => ({ label: c.name, value: c.code, flag: c.flag })) : []
}
onSelect={(v) => updateForm('country', v)}
/>
<CategorySelectorBottomSheet
isOpen={showRegion}
onClose={() => setShowRegion(false)}
selectedValue={formData.region}
data={regions.map((r) => ({ label: r.name, value: r.code }))}
data={[
{ label: t('Hammasi'), value: 'all' },
...regions.map((r) => ({ label: r.name, value: r.code })),
]}
onSelect={(v) => updateForm('region', v)}
/>
<CategorySelectorBottomSheet
isOpen={showDistrict}
onClose={() => setShowDistrict(false)}
selectedValue={formData.district}
data={districts.map((d) => ({ label: d.name, value: d.code }))}
data={[
{ label: t('Hammasi'), value: 'all' },
...districts.map((d) => ({ label: d.name, value: d.code })),
]}
onSelect={(v) => updateForm('district', v)}
/>
</ScrollView>
@@ -286,4 +351,14 @@ const styles = StyleSheet.create({
priceLine: { fontSize: 15 },
totalPrice: { fontSize: 18, fontWeight: '700', marginTop: 6 },
error: { fontWeight: '600', marginBottom: 10 },
selectAllButton: {
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 8,
borderWidth: 1,
},
selectAllText: {
fontSize: 14,
fontWeight: '600',
},
});

View File

@@ -5,7 +5,6 @@ import { Image } from 'expo-image';
import { router } from 'expo-router';
import { ChevronLeft, XIcon } from 'lucide-react-native';
import React, { useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ActivityIndicator, FlatList, Modal, Text, TouchableOpacity, View } from 'react-native';
import { RefreshControl } from 'react-native-gesture-handler';
import { SafeAreaView } from 'react-native-safe-area-context';
@@ -14,16 +13,15 @@ import { eservices_api } from '../lib/api';
const dark = {
bg: '#0f172a',
card: '#1f2937',
card: '#334155',
border: '#1e293b',
muted: '#334155',
text: '#f8fafc',
subText: '#cbd5f5',
text: '#E5B037',
subText: '#0B0F2C',
};
export default function EServicesCategoryScreen() {
const { isDark } = useTheme();
const { t } = useTranslation();
const [modalVisible, setModalVisible] = useState(false);
const webviewRef = useRef<WebView>(null);
const [webUrl, setWebUrl] = React.useState<string | null>(null);
@@ -88,7 +86,7 @@ export default function EServicesCategoryScreen() {
<FlatList
data={categories}
keyExtractor={(item) => `${item.id}-${item.name}`}
contentContainerStyle={{ gap: 12 }}
contentContainerStyle={{ gap: 12, paddingBottom: 80 }}
refreshControl={
<RefreshControl
refreshing={refreshing}
@@ -97,52 +95,34 @@ export default function EServicesCategoryScreen() {
colors={['#3b82f6']}
/>
}
ListHeaderComponent={() => (
<View>
<Text
style={{
fontSize: 22,
fontWeight: '800',
color: isDark ? dark.text : '#020617',
}}
>
{t('Davlat xizmatlari kategoriyalari')}
</Text>
<Text
style={{
fontSize: 14,
marginTop: 4,
color: isDark ? dark.subText : '#64748b',
}}
>
{t('Kerakli xizmat turini tanlang')}
</Text>
</View>
)}
renderItem={({ item }) => (
<TouchableOpacity
activeOpacity={0.7}
onPress={() => handlePress(item)}
style={{
backgroundColor: isDark ? dark.card : '#ffffff',
padding: 14,
marginHorizontal: 1,
backgroundColor: isDark ? '#FDFDFD' : '#ffffff',
borderRadius: 16,
flexDirection: 'row',
alignItems: 'center',
borderWidth: isDark ? 1 : 0,
borderColor: isDark ? dark.border : 'transparent',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.15,
shadowRadius: 4,
elevation: 2,
}}
>
<View
style={{
width: 120,
height: 70,
width: '100%',
height: 100,
borderRadius: 12,
backgroundColor: isDark ? dark.muted : '#e2e8f0',
alignItems: 'center',
justifyContent: 'center',
marginRight: 14,
padding: 2,
overflow: 'hidden',
}}
>
@@ -156,31 +136,6 @@ export default function EServicesCategoryScreen() {
<Text style={{ fontWeight: '700', color: dark.text }}>{item.name[0]}</Text>
)}
</View>
<View style={{ flex: 1 }}>
<Text
numberOfLines={2}
style={{
fontSize: 16,
fontWeight: item.id === 0 ? '700' : '600',
color: isDark ? dark.text : '#020617',
}}
>
{item.name}
</Text>
{item.id === 0 && (
<Text
style={{
fontSize: 13,
marginTop: 4,
color: dark.subText,
}}
>
Tezkor va qulay xizmat
</Text>
)}
</View>
</TouchableOpacity>
)}
/>

View File

@@ -190,8 +190,8 @@ export default function EServicesScreen() {
renderItem={renderItem}
numColumns={3}
columnWrapperStyle={{ justifyContent: 'space-between', marginBottom: 12 }}
contentContainerStyle={{ padding: 16, paddingBottom: 32 }}
onEndReached={() => hasNextPage && fetchNextPage()}
contentContainerStyle={{ padding: 16, paddingBottom: 80 }}
onEndReached={() => hasNextPage && !isFetchingNextPage && fetchNextPage()}
onEndReachedThreshold={0.4}
ListFooterComponent={
isFetchingNextPage ? (

15
screens/home/lib/hook.ts Normal file
View File

@@ -0,0 +1,15 @@
import { create } from "zustand";
type HomeStore = {
showFilter: boolean;
setShowFilter: (value: boolean) => void;
step: 'filter' | 'items';
setStep: (value: 'filter' | 'items') => void;
}
export const useHomeStore = create<HomeStore>((set) => ({
showFilter: false,
setShowFilter: (value: boolean) => set({ showFilter: value }),
step: 'filter',
setStep: (value: 'filter' | 'items') => set({ step: value }),
}))

View File

@@ -22,7 +22,7 @@ export interface ProductResponse {
id: number;
file: string;
}[];
category: { id: number; name: string; icon: string }[];
category: { id: number; name: string; icon_name: string | null }[];
}
export interface CompanyBody {
@@ -42,7 +42,7 @@ export interface CompanyBody {
export interface CompanyResponse {
id: number;
company_name: string;
company_name: string | null;
country_name: string;
region_name: string;
district_name: string;
@@ -99,6 +99,7 @@ export interface States {
districts: { id: number; name: string; code: string }[];
}[];
code: string;
flag: string;
}[];
}

View File

@@ -9,7 +9,7 @@ import SearchTabs from '@/components/ui/SearchTabs';
import { useTabSearch } from '@/hooks/useSearch';
import { TabKey } from '@/types';
import { useQueryClient } from '@tanstack/react-query';
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { Filter, Search } from 'lucide-react-native';
import React, { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -25,6 +25,7 @@ import {
} from 'react-native';
import { GestureHandlerRootView, RefreshControl } from 'react-native-gesture-handler';
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
import { useHomeStore } from '../lib/hook';
function Loading() {
return (
@@ -37,12 +38,12 @@ function Loading() {
export default function HomeScreen() {
const { isDark } = useTheme();
const [activeTab, setActiveTab] = useState<TabKey>('products');
const [step, setStep] = useState<'filter' | 'items'>('filter');
const [query, setQuery] = useState('');
const [showFilter, setShowFilter] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [filtered, setFiltered] = useState<{ id: number; company_name: string }[]>([]);
const { t } = useTranslation();
const { showFilter, setShowFilter, step, setStep } = useHomeStore();
const queryClient = useQueryClient();
@@ -115,10 +116,7 @@ export default function HomeScreen() {
/>
}
>
<Stack.Screen options={{ headerShown: false }} />
<View style={styles.content}>
{/* Qidiruv va filter */}
<View style={styles.searchSection}>
<View
style={[
@@ -169,8 +167,12 @@ export default function HomeScreen() {
presentationStyle="pageSheet"
onRequestClose={handleCloseFilter}
>
<SafeAreaProvider>
<SafeAreaView style={{ flex: 1 }}>
<SafeAreaProvider style={{ flex: 1 }}>
<SafeAreaView style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#fff' }}>
<StatusBar
style={isDark ? 'light' : 'dark'}
backgroundColor={isDark ? '#0f172a' : '#fff'}
/>
<FilterUI back={handleCloseFilter} setStep={setStep} setFiltered={setFiltered} />
</SafeAreaView>
</SafeAreaProvider>

View File

@@ -3,12 +3,12 @@ import { API_URLS } from '@/api/URLs';
import { ProductBody, ProductResponse } from '@/screens/home/lib/types';
import { AxiosResponse } from 'axios';
import {
ExployeesResponse,
MyAdsData,
MyAdsDataRes,
MyBonusesData,
NotificationListRes,
UserInfoResponseData,
ExployeesResponse,
MyAdsData,
MyAdsDataRes,
MyBonusesData,
NotificationListRes,
UserInfoResponseData,
} from './type';
export const user_api = {
@@ -36,8 +36,8 @@ export const user_api = {
person_type: 'employee' | 'legal_entity' | 'ytt' | 'band';
phone: string;
activate_types: number[];
age: number;
gender: 'male' | 'female';
age: number | null;
gender: 'male' | 'female' | null;
}) {
const res = await httpClient.patch(API_URLS.User_Update, body);
return res;
@@ -136,4 +136,9 @@ export const user_api = {
const res = await httpClient.post(API_URLS.Notification_Ready(id));
return res;
},
async mark_all_as_read() {
const res = await httpClient.post(API_URLS.Notification_Mark_All_Read);
return res;
},
};

View File

@@ -17,7 +17,6 @@ import {
ToastAndroid,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { user_api } from '../lib/api';
export default function AddEmployee() {
@@ -73,7 +72,7 @@ export default function AddEmployee() {
};
return (
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
<View style={[styles.container, { backgroundColor: theme.background }]}>
<View style={styles.topHeader}>
<Pressable onPress={() => router.push('/profile/employees')}>
<ArrowLeft color={theme.text} />
@@ -140,7 +139,7 @@ export default function AddEmployee() {
</View>
</View>
</ScrollView>
</SafeAreaView>
</View>
);
}

View File

@@ -14,7 +14,6 @@ import {
Text,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { user_api } from '../lib/api';
import StepOneServices from './StepOneService';
@@ -127,7 +126,7 @@ export default function AddService() {
);
return (
<SafeAreaView style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#f8fafc' }}>
<View style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#f8fafc' }}>
<View style={[styles.header, { backgroundColor: isDark ? '#0f172a' : '#ffffff' }]}>
<View
style={{ flexDirection: 'row', gap: 10, alignContent: 'center', alignItems: 'center' }}
@@ -176,7 +175,7 @@ export default function AddService() {
)}
{step === 2 && <StepTwo ref={stepTwoRef} formData={formData} updateForm={updateForm} />}
</ScrollView>
</SafeAreaView>
</View>
);
}

View File

@@ -1,7 +1,9 @@
import PAYME from '@/assets/images/Payme_NEW.png';
import { useTheme } from '@/components/ThemeContext';
import { price_calculation } from '@/screens/create-ads/lib/api';
import { Ionicons } from '@expo/vector-icons';
import { BottomSheetBackdrop, BottomSheetModal, BottomSheetScrollView } from '@gorhom/bottom-sheet';
import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
import { useInfiniteQuery, useMutation, useQuery } from '@tanstack/react-query';
import { ResizeMode, Video } from 'expo-av';
import { useRouter } from 'expo-router';
import { ArrowLeft, EyeIcon, Megaphone, Plus } from 'lucide-react-native';
@@ -9,18 +11,21 @@ import React, { useCallback, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
ActivityIndicator,
Alert,
Dimensions,
FlatList,
Image,
Linking,
Pressable,
RefreshControl,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { user_api } from '../lib/api';
import { MyAdsDataRes } from '../lib/type';
const PAGE_SIZE = 10;
const { width } = Dimensions.get('window');
@@ -47,8 +52,8 @@ export function AnnouncementsTab() {
};
const [refreshing, setRefreshing] = useState(false);
const [selectedAnnouncement, setSelectedAnnouncement] = useState<any>(null);
const [sheetOpen, setSheetOpen] = useState(false);
const [selectedAnnouncement, setSelectedAnnouncement] = useState<MyAdsDataRes | null>(null);
const [sheetOpen, setSheetOpen] = useState(false); const bottomSheetModalRef = useRef<BottomSheetModal>(null);
const { data, isLoading, isError, fetchNextPage, hasNextPage, refetch } = useInfiniteQuery({
queryKey: ['my_ads'],
@@ -78,12 +83,12 @@ export function AnnouncementsTab() {
isError: detailError,
} = useQuery({
queryKey: ['my_ads_id', selectedAnnouncement?.id],
queryFn: () => user_api.my_ads_detail(selectedAnnouncement.id),
queryFn: () => user_api.my_ads_detail(selectedAnnouncement?.id!),
select: (res) => res.data.data,
enabled: !!selectedAnnouncement && sheetOpen,
});
const openSheet = (item: any) => {
const openSheet = (item: MyAdsDataRes) => {
setSelectedAnnouncement(item);
setSheetOpen(true);
requestAnimationFrame(() => bottomSheetRef.current?.present());
@@ -125,9 +130,35 @@ export function AnnouncementsTab() {
const formatAmount = (amount: number) => new Intl.NumberFormat('uz-UZ').format(amount) + " so'm";
const { mutate: payment } = useMutation({
mutationFn: (body: { return_url: string; adId: number; paymentType: 'payme' | 'referral' }) =>
price_calculation.payment(body),
onSuccess: async (res, variables) => {
if (variables.paymentType === 'payme') {
await Linking.openURL(res.data.url);
router.push('/profile/my-ads');
} else {
router.push('/profile/my-ads');
}
},
onError: (err) => {
Alert.alert('Xatolik yuz berdi', err.message);
},
});
const sendPayment = ({ type, id }: { id: number, type: 'payme' | 'referral' }) => {
payment({
adId: id,
paymentType: type,
return_url: 'https://infotarget.uz/en/main/dashboard',
});
};
if (isLoading) {
return (
<SafeAreaView>
<View>
<View style={styles.topHeader}>
<Pressable onPress={() => router.push('/profile')}>
<ArrowLeft color={theme.text} />
@@ -138,7 +169,7 @@ export function AnnouncementsTab() {
</Pressable>
</View>
<ActivityIndicator size={'large'} />
</SafeAreaView>
</View>
);
}
@@ -304,6 +335,63 @@ export function AnnouncementsTab() {
</>
)}
</BottomSheetScrollView>
{detail?.status === 'pending' && (
<View style={[styles.footerContainer, { backgroundColor: theme.sheetBg }]}>
<TouchableOpacity
style={[styles.paymentButton, { backgroundColor: theme.primary }]}
onPress={() => {
bottomSheetModalRef.current?.present();
}}
>
<Text style={styles.paymentButtonText}>{t("To'lov qilish")}</Text>
</TouchableOpacity>
</View>
)}
</BottomSheetModal>
<BottomSheetModal
ref={bottomSheetModalRef}
index={0}
snapPoints={['70%', '95%']}
backdropComponent={renderBackdrop}
handleIndicatorStyle={{ backgroundColor: '#94a3b8', width: 50 }}
backgroundStyle={{ backgroundColor: isDark ? '#0f172a' : '#ffffff' }}
enablePanDownToClose
>
<BottomSheetScrollView
style={styles.sheetContent}
contentContainerStyle={styles.sheetContentContainer}
>
<View style={{ padding: 20 }}>
<Text style={[styles.sheetTitle, isDark ? styles.darkText : styles.lightText]}>
{t("To'lov turini tanlang")}
</Text>
<TouchableOpacity
style={[
styles.paymentItem,
isDark ? styles.darkPaymentItem : styles.lightPaymentItem,
{ flexDirection: 'row', justifyContent: 'flex-start', alignItems: 'center' },
]}
onPress={() => sendPayment({ id: selectedAnnouncement?.id!, type: 'payme' })}
>
<Image source={PAYME} style={{ width: 80, height: 80 }} />
</TouchableOpacity>
<TouchableOpacity
style={[
styles.paymentItem,
isDark ? styles.darkPaymentItem : styles.lightPaymentItem,
]}
onPress={() => sendPayment({ id: selectedAnnouncement?.id!, type: 'referral' })}
>
<Text style={[styles.paymentText, isDark ? styles.darkText : styles.lightText]}>
{t('Referal orqali')}
</Text>
</TouchableOpacity>
</View>
</BottomSheetScrollView>
</BottomSheetModal>
</View>
);
@@ -321,7 +409,17 @@ const styles = StyleSheet.create({
},
headerTitle: { fontSize: 18, fontWeight: '700' },
list: { padding: 16, gap: 12 },
sheetContent: { flex: 1 },
sheetContentContainer: { paddingBottom: 40 },
darkText: {
color: '#f1f5f9',
},
lightText: {
color: '#0f172a',
},
list: { padding: 16, paddingBottom: 30, gap: 12 },
card: { borderRadius: 16, padding: 16, gap: 8 },
cardImage: { width: '100%', height: 160, borderRadius: 12 },
@@ -329,6 +427,24 @@ const styles = StyleSheet.create({
title: { fontSize: 16, fontWeight: '700' },
desc: { lineHeight: 20 },
paymentItem: {
height: 56,
borderRadius: 14,
justifyContent: 'center',
paddingHorizontal: 16,
marginBottom: 12,
},
darkPaymentItem: {
backgroundColor: '#1e293b',
},
lightPaymentItem: {
backgroundColor: '#f8fafc',
},
paymentText: {
fontSize: 16,
fontWeight: '600',
},
footer: { flexDirection: 'row', justifyContent: 'space-between' },
metaText: {},
date: {},
@@ -363,6 +479,25 @@ const styles = StyleSheet.create({
value: { flex: 1 },
price: { fontWeight: '700' },
footerContainer: {
padding: 16,
paddingBottom: 20,
borderTopWidth: 1,
borderTopColor: '#e2e8f0',
},
paymentButton: {
paddingVertical: 14,
paddingHorizontal: 24,
borderRadius: 12,
alignItems: 'center',
justifyContent: 'center',
},
paymentButtonText: {
color: '#ffffff',
fontSize: 16,
fontWeight: '700',
},
loading: {},
error: {},
});

View File

@@ -6,7 +6,6 @@ import { ArrowLeft, Award, Percent } from 'lucide-react-native';
import { useTranslation } from 'react-i18next';
import { ActivityIndicator, FlatList, Pressable, StyleSheet, Text, View } from 'react-native';
import { RefreshControl } from 'react-native-gesture-handler';
import { SafeAreaView } from 'react-native-safe-area-context';
import { user_api } from '../lib/api';
const PAGE_SIZE = 10;
@@ -60,7 +59,7 @@ export default function BonusesScreen() {
}
return (
<SafeAreaView style={[styles.container, { backgroundColor: isDark ? '#0f172a' : '#f8fafc' }]}>
<View style={[styles.container, { backgroundColor: isDark ? '#0f172a' : '#f8fafc' }]}>
<View style={[styles.topHeader, { backgroundColor: isDark ? '#0f172a' : '#ffffff' }]}>
<Pressable onPress={() => router.push('/profile')}>
<ArrowLeft color={isDark ? '#fff' : '#0f172a'} />
@@ -153,7 +152,7 @@ export default function BonusesScreen() {
</View>
}
/>
</SafeAreaView>
</View>
);
}
@@ -164,6 +163,7 @@ const styles = StyleSheet.create({
list: {
padding: 16,
gap: 16,
paddingBottom: 30,
},
card: {
borderRadius: 20,

View File

@@ -15,7 +15,6 @@ import {
ToastAndroid,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { user_api } from '../lib/api';
type FormType = {
@@ -89,7 +88,7 @@ export default function CreateReferrals() {
};
return (
<SafeAreaView style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#f8fafc' }}>
<View style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#f8fafc' }}>
{/* HEADER */}
<View style={[styles.header, { backgroundColor: isDark ? '#0f172a' : '#fff' }]}>
<Pressable onPress={() => router.back()}>
@@ -187,7 +186,7 @@ export default function CreateReferrals() {
</>
)}
</ScrollView>
</SafeAreaView>
</View>
);
}

View File

@@ -6,7 +6,6 @@ import { ArrowLeft, Loader } from 'lucide-react-native';
import { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Alert, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { user_api } from '../lib/api';
import StepOneServices from './StepOneService';
@@ -145,7 +144,7 @@ export default function EditService() {
};
return (
<SafeAreaView style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#f8fafc' }}>
<View style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#f8fafc' }}>
<View style={[styles.header, { backgroundColor: isDark ? '#0f172a' : '#ffffff' }]}>
<View style={{ flexDirection: 'row', gap: 10, alignItems: 'center' }}>
<Pressable onPress={handleBack}>
@@ -179,7 +178,7 @@ export default function EditService() {
)}
{step === 2 && <StepTwo ref={stepTwoRef} formData={formData} updateForm={updateForm} />}
</ScrollView>
</SafeAreaView>
</View>
);
}

View File

@@ -14,7 +14,6 @@ import {
Text,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { user_api } from '../lib/api';
import { ExployeesDataResponse } from '../lib/type';
@@ -84,7 +83,7 @@ export function EmployeesTab() {
if (isLoading) {
return (
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
<View style={[styles.container, { backgroundColor: theme.background }]}>
<View style={styles.topHeader}>
<Pressable onPress={() => router.push('/profile')}>
<ArrowLeft color={theme.text} />
@@ -95,7 +94,7 @@ export function EmployeesTab() {
</Pressable>
</View>
<ActivityIndicator size={'large'} />
</SafeAreaView>
</View>
);
}
@@ -110,7 +109,7 @@ export function EmployeesTab() {
}
return (
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
<View style={[styles.container, { backgroundColor: theme.background }]}>
<View style={styles.topHeader}>
<Pressable onPress={() => router.push('/profile')}>
<ArrowLeft color={theme.text} />
@@ -152,14 +151,14 @@ export function EmployeesTab() {
</View>
}
/>
</SafeAreaView>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
addButton: { padding: 8 },
list: { padding: 16, gap: 12 },
list: { padding: 16, gap: 12, paddingBottom: 30 },
card: {
flexDirection: 'row',
alignItems: 'center',

View File

@@ -1,24 +1,22 @@
import { useTheme } from '@/components/ThemeContext';
import { router } from 'expo-router';
import { VideoView, useVideoPlayer } from 'expo-video';
import { ArrowLeft, Check, ChevronDown, X } from 'lucide-react-native';
import React, { useMemo, useState } from 'react';
import { ArrowLeft, Play, X } from 'lucide-react-native';
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Dimensions,
FlatList,
Image,
Pressable,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import ImageViewer from 'react-native-image-zoom-viewer';
import Modal from 'react-native-modal';
const { width } = Dimensions.get('window');
type ManualStep = {
image: any;
description?: string;
@@ -39,6 +37,7 @@ const languages: Language[] = [
export function ManualTab() {
const { isDark } = useTheme();
const { t, i18n } = useTranslation();
const [isPlaying, setIsPlaying] = useState(false);
/** 🔹 Modal states */
const [imageVisible, setImageVisible] = useState(false);
@@ -69,9 +68,9 @@ export function ManualTab() {
/** 🔹 Video manbalari (SELECT ga bogliq) */
const videos = {
uz: require('@/assets/manual/manual_video_uz.mp4'),
ru: require('@/assets/manual/manual_video_ru.mp4'),
en: require('@/assets/manual/manual_video_en.mp4'),
uz: require('@/assets/manual/manual_video_uz.webm'),
ru: require('@/assets/manual/manual_video_ru.webm'),
en: require('@/assets/manual/manual_video_en.webm'),
};
const player = useVideoPlayer(videos[selectedLang], (player) => {
@@ -161,6 +160,17 @@ export function ManualTab() {
const selectedLanguage = languages.find((l) => l.code === selectedLang);
useEffect(() => {
// listener qo'shish
const subscription = player.addListener('playingChange', (state) => {
setIsPlaying(state.isPlaying);
});
return () => {
subscription.remove();
};
}, [player]);
const renderStep = ({ item, index }: { item: ManualStep; index: number }) => (
<Pressable
onPress={() => {
@@ -186,18 +196,17 @@ export function ManualTab() {
{t("Foydalanish qo'llanmasi")}
</Text>
</View>
<Text style={[styles.subtitle, { color: theme.textMuted }]}>
{t("Quyidagi qisqa video yoki rasmlarni ko'rib chiqing")}
</Text>
</View>
{/* VIDEO */}
<View style={styles.section}>
<Text style={[styles.sectionTitle, { color: theme.text }]}>
{t("Foydalanish video qo'llanma")}
{t("Foydalanish qo'llanmasi")}
</Text>
<View style={{ alignContent: 'flex-end', alignItems: 'flex-end', paddingHorizontal: 16 }}>
<Text style={[styles.subtitle, { color: theme.textMuted }]}>
{t("Quyidagi qisqa video yoki rasmlarni ko'rib chiqing")}
</Text>
{/* <View style={{ alignContent: 'flex-end', alignItems: 'flex-end', paddingHorizontal: 16 }}>
<Pressable
style={[
styles.langSelector,
@@ -210,7 +219,7 @@ export function ManualTab() {
</View>
<ChevronDown color={theme.textMuted} />
</Pressable>
</View>
</View> */}
<View style={[styles.videoCard, { backgroundColor: theme.cardBg }]}>
<VideoView
@@ -218,16 +227,49 @@ export function ManualTab() {
style={styles.video}
allowsFullscreen
allowsPictureInPicture
nativeControls={true}
contentFit="cover"
/>
{!isPlaying && (
<View
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
justifyContent: 'center',
alignItems: 'center',
}}
>
<TouchableOpacity
style={{
backgroundColor: 'white',
borderRadius: 50,
width: 40,
height: 40,
justifyContent: 'center',
alignContent: 'center',
alignItems: 'center',
}}
onPress={() => {
player.play();
setIsPlaying(true);
}}
>
<Play color={'white'} size={26} fill={'black'} />
</TouchableOpacity>
</View>
)}
</View>
</View>
{/* RASMLAR */}
<View style={styles.section}>
<Text style={[styles.sectionTitle, { color: theme.text }]}>
{/* <Text style={[styles.sectionTitle, { color: theme.text }]}>
{t('Foydalanish rasm qollanma')}
</Text>
</Text> */}
<FlatList
horizontal
@@ -254,7 +296,7 @@ export function ManualTab() {
</Modal>
{/* LANGUAGE MODAL */}
<Modal isVisible={langPickerVisible} onBackdropPress={() => setLangPickerVisible(false)}>
{/* <Modal isVisible={langPickerVisible} onBackdropPress={() => setLangPickerVisible(false)}>
<View style={[styles.langModalContent, { backgroundColor: theme.cardBg }]}>
<Text style={{ color: 'white', marginBottom: 10, fontSize: 18 }}>
{t('Video tilini tanlang')}
@@ -299,7 +341,7 @@ export function ManualTab() {
);
})}
</View>
</Modal>
</Modal> */}
</ScrollView>
);
}
@@ -308,8 +350,8 @@ const styles = StyleSheet.create({
container: { flex: 1 },
hero: { padding: 20 },
topHeader: { flexDirection: 'row', alignItems: 'center', gap: 12 },
headerTitle: { fontSize: 22, fontWeight: '700' },
subtitle: { marginTop: 8 },
headerTitle: { fontSize: 22, fontWeight: '700', marginHorizontal: 16 },
subtitle: { fontSize: 16, marginTop: 5, fontWeight: '500', marginHorizontal: 16 },
section: { marginBottom: 28 },
sectionTitle: { fontSize: 20, fontWeight: '700', marginLeft: 16 },
@@ -329,6 +371,7 @@ const styles = StyleSheet.create({
videoCard: {
marginHorizontal: 16,
marginTop: 10,
borderRadius: 20,
overflow: 'hidden',
position: 'relative',

View File

@@ -1,5 +1,4 @@
import { useTheme } from '@/components/ThemeContext';
import { useGlobalRefresh } from '@/components/ui/RefreshContext';
import { useInfiniteQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Image as ExpoImage } from 'expo-image';
import { useRouter } from 'expo-router';
@@ -15,20 +14,18 @@ import {
View,
} from 'react-native';
import { RefreshControl } from 'react-native-gesture-handler';
import { SafeAreaView } from 'react-native-safe-area-context';
import { user_api } from '../lib/api';
const PAGE_SIZE = 5;
const PAGE_SIZE = 10;
export default function MyServicesScreen() {
const router = useRouter();
const { onRefresh, refreshing } = useGlobalRefresh();
const queryClient = useQueryClient();
const { isDark } = useTheme();
const { t } = useTranslation();
/* ================= QUERY ================= */
const { data, isLoading, isError, fetchNextPage, hasNextPage } = useInfiniteQuery({
const { data, isLoading, isError, fetchNextPage, hasNextPage, isRefetching } = useInfiniteQuery({
queryKey: ['my_services'],
queryFn: async ({ pageParam = 1 }) => {
const res = await user_api.my_sevices({
@@ -69,9 +66,13 @@ export default function MyServicesScreen() {
]);
};
const onRefresh = () => {
queryClient.refetchQueries({ queryKey: ['my_services'] });
};
if (isLoading) {
return (
<SafeAreaView style={[styles.container, { backgroundColor: isDark ? '#0f172a' : '#f8fafc' }]}>
<View style={[styles.container, { backgroundColor: isDark ? '#0f172a' : '#f8fafc' }]}>
<View style={[styles.topHeader, { backgroundColor: isDark ? '#0f172a' : '#ffffff' }]}>
<Pressable onPress={() => router.push('/profile')}>
<ArrowLeft color={isDark ? '#fff' : '#0f172a'} />
@@ -84,7 +85,7 @@ export default function MyServicesScreen() {
</Pressable>
</View>
<ActivityIndicator size={'large'} />
</SafeAreaView>
</View>
);
}
@@ -97,7 +98,7 @@ export default function MyServicesScreen() {
}
return (
<SafeAreaView style={[styles.container, { backgroundColor: isDark ? '#0f172a' : '#f8fafc' }]}>
<View style={[styles.container, { backgroundColor: isDark ? '#0f172a' : '#f8fafc' }]}>
{/* HEADER */}
<View style={[styles.topHeader, { backgroundColor: isDark ? '#0f172a' : '#ffffff' }]}>
<Pressable onPress={() => router.push('/profile')}>
@@ -119,7 +120,7 @@ export default function MyServicesScreen() {
onEndReached={() => hasNextPage && fetchNextPage()}
refreshControl={
<RefreshControl
refreshing={refreshing}
refreshing={isRefetching}
onRefresh={onRefresh}
colors={['#2563eb']}
tintColor="#2563eb"
@@ -235,7 +236,7 @@ export default function MyServicesScreen() {
</View>
}
/>
</SafeAreaView>
</View>
);
}
@@ -253,7 +254,7 @@ const styles = StyleSheet.create({
elevation: 3,
},
headerTitle: { fontSize: 18, fontWeight: '700', flex: 1, marginLeft: 10 },
list: { padding: 16, gap: 16 },
list: { padding: 16, gap: 16, paddingBottom: 30 },
card: { borderRadius: 20, overflow: 'hidden' },
mediaContainer: { width: '100%', height: 200 },
media: { width: '100%', height: '100%' },

View File

@@ -38,6 +38,15 @@ export function NotificationTab() {
initialPageParam: 1,
});
const notifications = data?.pages.flatMap((p) => p.results) ?? [];
const queryClient = useQueryClient();
const { mutate: markAllAsRead, isPending: isMarkingAllRead } = useMutation({
mutationFn: () => user_api.mark_all_as_read(),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['notifications-list'] });
queryClient.invalidateQueries({ queryKey: ['notification-list'] });
},
});
if (isLoading) {
return (
@@ -82,6 +91,25 @@ export function NotificationTab() {
<Text style={[styles.headerTitle, { color: isDark ? '#f1f5f9' : '#0f172a' }]}>
{t('Bildirishnomalar')}
</Text>
{notifications.some((n) => !n.is_read) && (
<TouchableOpacity
style={[
styles.markAllButton,
{ backgroundColor: isDark ? '#1e293b' : '#e0f2fe', borderColor: '#3b82f6' },
]}
onPress={() => markAllAsRead()}
disabled={isMarkingAllRead}
>
{isMarkingAllRead ? (
<ActivityIndicator size="small" color="#3b82f6" />
) : (
<Text style={[styles.markAllText, { color: '#3b82f6' }]}>
{t("Barchasi o'qildi")}
</Text>
)}
</TouchableOpacity>
)}
</View>
<FlatList
data={notifications}
@@ -372,5 +400,19 @@ const styles = StyleSheet.create({
fontSize: 18,
fontWeight: '700',
lineHeight: 24,
flex: 1,
},
markAllButton: {
paddingHorizontal: 12,
paddingVertical: 8,
borderRadius: 8,
borderWidth: 1,
minWidth: 80,
alignItems: 'center',
justifyContent: 'center',
},
markAllText: {
fontSize: 13,
fontWeight: '600',
},
});

View File

@@ -1,6 +1,8 @@
import AdsLogo from '@/assets/images/one_click.png';
import { useTheme } from '@/components/ThemeContext';
import { useGlobalRefresh } from '@/components/ui/RefreshContext';
import { useQuery } from '@tanstack/react-query';
import { Image } from 'expo-image';
import { useRouter } from 'expo-router';
import {
Award,
@@ -8,6 +10,7 @@ import {
BookAIcon,
ChevronRight,
HandCoins,
LucideIcon,
Megaphone,
Package,
Settings,
@@ -19,6 +22,11 @@ import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-nati
import { RefreshControl } from 'react-native-gesture-handler';
import { user_api } from '../lib/api';
interface SectionType {
title: string;
items: { icon: LucideIcon; label: string; route: string; badge?: number; image?: any }[];
}
export default function Profile() {
const router = useRouter();
const { onRefresh, refreshing } = useGlobalRefresh();
@@ -36,7 +44,7 @@ export default function Profile() {
queryFn: () => user_api.getMe(),
});
const sections = [
const sections: SectionType[] = [
{
title: 'Shaxsiy',
items: [
@@ -53,17 +61,17 @@ export default function Profile() {
{
title: 'Faoliyat',
items: [
{ icon: Megaphone, label: "E'lonlar", route: '/profile/my-ads' },
{ icon: Megaphone, label: "E'lonlar", image: AdsLogo, 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',
},
]
{
icon: HandCoins,
label: 'Refferallarim',
route: '/profile/my-referrals',
},
]
: []),
],
},
@@ -79,6 +87,7 @@ export default function Profile() {
return (
<ScrollView
style={[styles.content, isDark ? styles.darkBg : styles.lightBg]}
contentContainerStyle={{ paddingBottom: 90 }}
refreshControl={
<RefreshControl
refreshing={refreshing}
@@ -107,17 +116,32 @@ export default function Profile() {
>
<item.icon size={24} color="#3b82f6" />
{item.badge && (
{item?.badge && (
<View style={styles.badge}>
<Text style={styles.badgeText}>{item.badge}</Text>
</View>
)}
</View>
<Text style={[styles.cardLabel, isDark ? styles.darkText : styles.lightText]}>
{t(item.label)}
</Text>
<View style={{ flex: 1 }}>
{item.image ? (
<View style={{ flex: 1 }}>
<Image
source={item.image}
style={{ width: '60%', height: 40 }}
contentFit="contain"
/>
<Text style={[styles.cardLabel, isDark ? styles.darkText : styles.lightText, { fontSize: 14, marginTop: 4, fontWeight: '500' }]}>
{t("Bir Zumda Jonatish")}
</Text>
</View>
) : (
<View>
<Text style={[styles.cardLabel, isDark ? styles.darkText : styles.lightText]}>
{t(item.label)}
</Text>
</View>
)}
</View>
<ChevronRight size={20} color={isDark ? '#64748b' : '#94a3b8'} />
</TouchableOpacity>
))}
@@ -131,7 +155,7 @@ export default function Profile() {
const styles = StyleSheet.create({
content: {
flex: 1,
marginBottom: 50,
paddingBottom: 120,
},
darkBg: {

View File

@@ -8,8 +8,10 @@ import { useTranslation } from 'react-i18next';
import {
ActivityIndicator,
FlatList,
Platform,
Pressable,
RefreshControl,
Share,
StyleSheet,
Text,
ToastAndroid,
@@ -64,9 +66,31 @@ export function ReferralsTab() {
setRefreshing(false);
};
// Clipboard + Share funksiyasi
const handleCopyAndShare = async (code: string) => {
const referralLink = `https://t.me/infotargetbot/join?startapp=${code}`;
// Clipboard-ga nusxa olish
await Clipboard.setStringAsync(referralLink);
// Share qilish
try {
await Share.share({
message: referralLink,
title: t('Referal linkni ulashish'),
});
} catch (err) {
console.log('Share error:', err);
}
if (Platform.OS === 'android') {
ToastAndroid.show(t('Refferal kopiya qilindi'), ToastAndroid.SHORT);
}
};
if (isLoading) {
return (
<SafeAreaView style={{ flex: 1 }}>
<SafeAreaView style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator size="large" />
</SafeAreaView>
);
@@ -103,26 +127,12 @@ export function ReferralsTab() {
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.cardRow}>
<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);
}}
>
<TouchableOpacity onPress={() => handleCopyAndShare(item.code)}>
<CopyIcon size={20} color={theme.primary} />
</TouchableOpacity>
</View>
@@ -165,7 +175,7 @@ const styles = StyleSheet.create({
},
headerTitle: { fontSize: 18, fontWeight: '700' },
list: { padding: 16, gap: 12 },
list: { padding: 16, gap: 12, paddingBottom: 30 },
card: {
borderRadius: 16,
@@ -173,6 +183,13 @@ const styles = StyleSheet.create({
gap: 10,
},
cardRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignContent: 'center',
alignItems: 'center',
},
cardHeader: {
flexDirection: 'row',
gap: 8,