Files
info-target-mobile/screens/profile/ui/AnnouncementsTab.tsx
Samandar Turgunboyev ab363ca3b9 bug fixed
2026-03-02 13:22:55 +05:00

527 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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, 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';
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 { user_api } from '../lib/api';
import { MyAdsDataRes } from '../lib/type';
const PAGE_SIZE = 10;
const { width } = Dimensions.get('window');
export function AnnouncementsTab() {
const router = useRouter();
const bottomSheetRef = useRef<BottomSheetModal>(null);
const { isDark } = useTheme();
const { t } = useTranslation();
const theme = {
background: isDark ? '#0f172a' : '#f8fafc',
cardBg: isDark ? '#1e293b' : '#ffffff',
text: isDark ? '#ffffff' : '#0f172a',
textSecondary: isDark ? '#94a3b8' : '#64748b',
textTertiary: isDark ? '#fff6' : '#64748b',
primary: '#3b82f6',
sheetBg: isDark ? '#0f172a' : '#ffffff',
indicator: isDark ? '#94a3b8' : '#cbd5e1',
statusBadge: '#2563eb',
typeColor: '#38bdf8',
priceColor: '#10b981',
error: '#ef4444',
};
const [refreshing, setRefreshing] = useState(false);
const [selectedAnnouncement, setSelectedAnnouncement] = useState<number | null>(null);
const [sheetOpen, setSheetOpen] = useState(false); const bottomSheetModalRef = useRef<BottomSheetModal>(null);
const { data, isLoading, isError, fetchNextPage, hasNextPage, refetch } = useInfiniteQuery({
queryKey: ['my_ads'],
queryFn: async ({ pageParam = 1 }) => {
const res = await user_api.my_ads({
page: pageParam,
page_size: PAGE_SIZE,
});
const d = res?.data?.data;
return {
results: d?.results ?? [],
current_page: d?.current_page ?? 1,
total_pages: d?.total_pages ?? 1,
};
},
getNextPageParam: (lastPage) =>
lastPage.current_page < lastPage.total_pages ? lastPage.current_page + 1 : undefined,
initialPageParam: 1,
});
const allAds = data?.pages.flatMap((p) => p.results) ?? [];
const {
data: detail,
isLoading: loadingDetail,
isError: detailError,
} = useQuery({
queryKey: ['my_ads_id', selectedAnnouncement],
queryFn: () => user_api.ads_detail(Number(selectedAnnouncement)),
select: (res) => res.data.data,
enabled: !!selectedAnnouncement,
});
const openSheet = (item: MyAdsDataRes) => {
setSelectedAnnouncement(item.id);
setSheetOpen(true);
requestAnimationFrame(() => bottomSheetRef.current?.present());
};
const onRefresh = async () => {
setRefreshing(true);
await refetch();
setRefreshing(false);
};
const renderBackdrop = useCallback(
(props: any) => (
<BottomSheetBackdrop {...props} appearsOnIndex={0} disappearsOnIndex={-1} opacity={0.4} />
),
[]
);
const getStatusColor = (status: string) => {
switch (status) {
case 'paid':
case 'verified':
return '#10b981';
case 'pending':
return '#f59e0b';
case 'canceled':
return '#ef4444';
default:
return '#94a3b8';
}
};
const statusLabel: Record<string, string> = {
pending: 'Kutilmoqda',
paid: "To'langan",
verified: 'Tasdiqlangan',
canceled: 'Bekor qilingan',
};
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 (
<View>
<View style={styles.topHeader}>
<Pressable onPress={() => router.push('/profile')}>
<ArrowLeft color={theme.text} />
</Pressable>
<Text style={[styles.headerTitle, { color: theme.text }]}>{t("E'lonlar")}</Text>
<Pressable onPress={() => router.push('/(dashboard)/create-announcements')}>
<Plus color={theme.primary} />
</Pressable>
</View>
<ActivityIndicator size={'large'} />
</View>
);
}
if (isError) {
return (
<View style={[styles.center, { backgroundColor: theme.background }]}>
<Text style={[{ color: theme.error }]}>{t('Xatolik yuz berdi')}</Text>
</View>
);
}
return (
<View style={[styles.container, { backgroundColor: theme.background }]}>
<View style={styles.topHeader}>
<Pressable onPress={() => router.push('/profile')}>
<ArrowLeft color={theme.text} />
</Pressable>
<Text style={[styles.headerTitle, { color: theme.text }]}>{t("E'lonlar")}</Text>
<Pressable onPress={() => router.push('/(dashboard)/create-announcements')}>
<Plus color={theme.primary} />
</Pressable>
</View>
<FlatList
data={allAds}
keyExtractor={(item) => item.id.toString()}
contentContainerStyle={[styles.list, { flexGrow: 1 }]}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={theme.primary} />
}
onEndReached={() => hasNextPage && fetchNextPage()}
renderItem={({ item }) => (
<Pressable
style={[styles.card, { backgroundColor: theme.cardBg }]}
onPress={() => { openSheet(item); setSelectedAnnouncement(item.id) }}
>
{item.files?.[0]?.file && (
<Image source={{ uri: item.files[0].file }} style={styles.cardImage} />
)}
<View style={styles.cardHeader}>
<Megaphone size={18} color={theme.primary} />
<Text style={{ color: getStatusColor(item.status), fontWeight: '600' }}>
{t(statusLabel[item.status])}
</Text>
</View>
<Text style={[styles.title, { color: theme.text }]}>{item.title}</Text>
<Text numberOfLines={2} style={[styles.desc, { color: theme.textTertiary }]}>
{item.description}
</Text>
<View style={styles.footer}>
<View
style={{
flexDirection: 'row',
alignContent: 'center',
alignItems: 'center',
gap: 5,
}}
>
<EyeIcon size={20} color={theme.textSecondary} />
<Text style={[styles.metaText, { color: theme.textSecondary }]}>
{item.total_view_count}
</Text>
</View>
<Text style={[styles.date, { color: theme.textSecondary }]}>
{new Date(item.created_at).toLocaleDateString('uz-UZ')}
</Text>
</View>
</Pressable>
)}
ListEmptyComponent={() => (
<View style={styles.emptyContainer}>
<Megaphone size={48} color={theme.textSecondary} />
<Text style={[styles.emptyTitle, { color: theme.text }]}>
{t('Hozircha hech qanday eʼlon mavjud emas')}
</Text>
</View>
)}
/>
<BottomSheetModal
ref={bottomSheetRef}
snapPoints={['70%', '95%']}
backdropComponent={renderBackdrop}
enablePanDownToClose
backgroundStyle={{ backgroundColor: theme.sheetBg }}
handleIndicatorStyle={{ backgroundColor: theme.indicator }}
onDismiss={() => {
setSelectedAnnouncement(null); // shu yetarli
}}
>
<BottomSheetScrollView contentContainerStyle={styles.sheet}>
{loadingDetail && <ActivityIndicator size={'large'} />}
{detailError && (
<Text style={[{ color: theme.error }]}>{t('Xatolik yuz berdi')}</Text>
)}
{detail && (
<>
{detail.files?.length > 0 && (
<ScrollView horizontal pagingEnabled showsHorizontalScrollIndicator={false}>
{detail.files.map((file: any) => {
const isVideo = file.file.endsWith('.mp4');
return (
<View key={file.id} style={styles.mediaContainer}>
{isVideo ? (
<Video
source={{ uri: file.file }}
style={styles.media}
resizeMode={ResizeMode.CONTAIN}
useNativeControls
/>
) : (
<Image source={{ uri: file.file }} style={styles.media} />
)}
</View>
);
})}
</ScrollView>
)}
<Text style={[styles.sheetTitle, { color: theme.text }]}>{detail.title}</Text>
<View style={styles.metaRow}>
<Ionicons name="eye-outline" size={16} color={theme.textSecondary} />
<Text style={[styles.metaText, { color: theme.textSecondary }]}>
{detail.total_view_count} {t("ko'rildi")}
</Text>
</View>
<View style={[styles.statusBadge, { backgroundColor: theme.statusBadge }]}>
<Text style={styles.statusText}>{statusLabel[detail.status]}</Text>
</View>
<View style={styles.infoRowColumn}>
<Text style={[styles.label, { color: theme.textSecondary }]}>
{t('Kategoriyalar')}:
</Text>
{detail.types?.map((type: any) => (
<Text key={type.id} style={[styles.typeItem, { color: theme.typeColor }]}>
{type.name}
</Text>
))}
</View>
<View style={styles.infoRow}>
<Text style={[styles.label, { color: theme.textSecondary }]}>
{t('Tanlangan kompaniyalar')}:
</Text>
<Text style={[styles.value, { color: theme.text }]}>
{detail.letters?.join(', ')}
</Text>
</View>
<View style={styles.infoRow}>
<Text style={[styles.label, { color: theme.textSecondary }]}>{t('Narxi')}:</Text>
<Text style={[styles.price, { color: theme.priceColor }]}>
{formatAmount(detail.total_price)}
</Text>
</View>
<View style={styles.infoRow}>
<Text style={[styles.label, { color: theme.textSecondary }]}>{t('Tavsif')}:</Text>
<Text style={[styles.desc, { color: theme.textTertiary }]}>
{detail.description}
</Text>
</View>
</>
)}
</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!, type: 'payme' })}
>
<Image source={PAYME} style={{ width: 80, height: 80 }} />
</TouchableOpacity>
<TouchableOpacity
style={[
styles.paymentItem,
isDark ? styles.darkPaymentItem : styles.lightPaymentItem,
]}
onPress={() => sendPayment({ id: selectedAnnouncement!, type: 'referral' })}
>
<Text style={[styles.paymentText, isDark ? styles.darkText : styles.lightText]}>
{t('Referal orqali')}
</Text>
</TouchableOpacity>
</View>
</BottomSheetScrollView>
</BottomSheetModal>
</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' },
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 },
cardHeader: { flexDirection: 'row', justifyContent: 'space-between' },
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: {},
sheet: { padding: 20, gap: 12 },
sheetTitle: { fontSize: 18, fontWeight: '700' },
mediaContainer: { width: width - 40, height: 200, marginRight: 12 },
media: { width: '100%', height: '100%', borderRadius: 12 },
metaRow: { flexDirection: 'row', gap: 6, alignItems: 'center' },
statusBadge: {
alignSelf: 'flex-start',
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 12,
},
statusText: { color: '#fff', fontWeight: '600' },
infoRow: { flexDirection: 'column', gap: 6 },
infoRowColumn: {
marginTop: 8,
gap: 4,
},
typeItem: {
fontSize: 14,
},
label: {},
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',
},
emptyContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 60,
},
emptyTitle: {
fontSize: 18,
fontWeight: '600',
marginTop: 12,
},
emptyDesc: {
fontSize: 14,
marginTop: 6,
textAlign: 'center',
},
});