527 lines
16 KiB
TypeScript
527 lines
16 KiB
TypeScript
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',
|
||
},
|
||
});
|