404 lines
12 KiB
TypeScript
404 lines
12 KiB
TypeScript
import { useTheme } from '@/components/ThemeContext';
|
||
import { products_api } from '@/screens/home/lib/api';
|
||
import { ProductResponse } from '@/screens/home/lib/types';
|
||
import { user_api } from '@/screens/profile/lib/api';
|
||
import { BottomSheetBackdrop, BottomSheetModal, BottomSheetScrollView } from '@gorhom/bottom-sheet';
|
||
import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
|
||
import { ResizeMode, Video } from 'expo-av';
|
||
import { Info, Package, PlayCircle } from 'lucide-react-native';
|
||
import React, { useCallback, useRef, useState } from 'react';
|
||
import { useTranslation } from 'react-i18next';
|
||
import {
|
||
ActivityIndicator,
|
||
Dimensions,
|
||
Image,
|
||
StyleSheet,
|
||
Text,
|
||
TouchableOpacity,
|
||
View,
|
||
} from 'react-native';
|
||
import { FlatList } from 'react-native-gesture-handler';
|
||
|
||
const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
||
const IMAGE_WIDTH = SCREEN_WIDTH - 40;
|
||
const PAGE_SIZE = 10;
|
||
|
||
type Props = { query: string };
|
||
|
||
export default function ProductList({ query }: Props) {
|
||
const { t } = useTranslation();
|
||
const { isDark } = useTheme();
|
||
const trimmedQuery = query.trim();
|
||
const [selectedProduct, setSelectedProduct] = useState<ProductResponse | null>(null);
|
||
const [currentImageIndex, setCurrentImageIndex] = useState(0);
|
||
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
||
|
||
const { data, isLoading, isError, fetchNextPage, hasNextPage, isFetchingNextPage } =
|
||
useInfiniteQuery({
|
||
queryKey: ['products-list', trimmedQuery],
|
||
queryFn: async ({ pageParam = 1 }) => {
|
||
const response = await products_api.getProducts({
|
||
page: pageParam,
|
||
page_size: PAGE_SIZE,
|
||
search: trimmedQuery,
|
||
});
|
||
return response.data.data;
|
||
},
|
||
getNextPageParam: (lastPage) =>
|
||
lastPage.current_page < lastPage.total_pages ? lastPage.current_page + 1 : undefined,
|
||
initialPageParam: 1,
|
||
});
|
||
|
||
const allProducts = data?.pages.flatMap((p) => p.results) ?? [];
|
||
|
||
const {
|
||
data: detail,
|
||
isLoading: loadingDetail,
|
||
isError: detailError,
|
||
} = useQuery({
|
||
queryKey: ['my_ads_id', selectedProduct],
|
||
queryFn: () => user_api.detail_service(Number(selectedProduct?.id)),
|
||
select: (res) => res.data.data,
|
||
enabled: !!selectedProduct,
|
||
});
|
||
|
||
const handlePresentModalPress = useCallback((product: ProductResponse) => {
|
||
setSelectedProduct(product);
|
||
setCurrentImageIndex(0);
|
||
bottomSheetModalRef.current?.present();
|
||
}, []);
|
||
|
||
const renderBackdrop = useCallback(
|
||
(props: any) => (
|
||
<BottomSheetBackdrop {...props} disappearsOnIndex={-1} appearsOnIndex={0} opacity={0.5} />
|
||
),
|
||
[]
|
||
);
|
||
|
||
const isVideoFile = (url?: string) => url?.toLowerCase().match(/\.(mp4|mov|avi|mkv|webm)$/i);
|
||
|
||
const renderItem = ({ item }: { item: ProductResponse }) => {
|
||
const mainFile = item.files?.[0]?.file;
|
||
const isVideo = isVideoFile(mainFile);
|
||
|
||
return (
|
||
<TouchableOpacity
|
||
style={[styles.card, isDark ? styles.darkCard : styles.lightCard]}
|
||
activeOpacity={0.85}
|
||
onPress={() => handlePresentModalPress(item)}
|
||
>
|
||
<View style={[styles.imageContainer, isDark ? styles.darkImageBg : styles.lightImageBg]}>
|
||
{mainFile ? (
|
||
<>
|
||
<Image source={{ uri: mainFile }} style={styles.image} resizeMode="cover" />
|
||
{isVideo && (
|
||
<View style={styles.videoIconOverlay}>
|
||
<PlayCircle size={36} color="white" fill="rgba(0,0,0,0.35)" />
|
||
</View>
|
||
)}
|
||
</>
|
||
) : (
|
||
<View style={styles.placeholder}>
|
||
<Package size={40} color={isDark ? '#64748b' : '#cbd5e1'} />
|
||
</View>
|
||
)}
|
||
</View>
|
||
<View style={styles.info}>
|
||
<Text
|
||
style={[styles.title, isDark ? styles.darkText : styles.lightText]}
|
||
numberOfLines={2}
|
||
>
|
||
{item.title}
|
||
</Text>
|
||
<View style={styles.companyRow}>
|
||
<Text style={[styles.companyText, isDark ? styles.darkSubText : styles.lightSubText]}>
|
||
{item.company}
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
</TouchableOpacity>
|
||
);
|
||
};
|
||
|
||
const renderCarouselItem = ({ item }: { item: { id: number; file: string } }) => {
|
||
const isVideo = isVideoFile(item.file);
|
||
return (
|
||
<View style={styles.carouselImageContainer}>
|
||
{isVideo ? (
|
||
<Video
|
||
source={{ uri: item.file }}
|
||
style={styles.carouselImage}
|
||
resizeMode={ResizeMode.COVER}
|
||
useNativeControls
|
||
/>
|
||
) : (
|
||
<Image source={{ uri: item.file }} style={styles.carouselImage} resizeMode="cover" />
|
||
)}
|
||
</View>
|
||
);
|
||
};
|
||
|
||
if (isLoading && !data)
|
||
return (
|
||
<View style={styles.center}>
|
||
<ActivityIndicator size="large" color="#3b82f6" />
|
||
</View>
|
||
);
|
||
|
||
if (isError)
|
||
return (
|
||
<View style={styles.center}>
|
||
<Text style={{ color: '#f87171' }}>{t('Xatolik yuz berdi')}</Text>
|
||
</View>
|
||
);
|
||
|
||
return (
|
||
<>
|
||
<FlatList
|
||
data={allProducts}
|
||
nestedScrollEnabled={true}
|
||
keyExtractor={(item) => item.id.toString()}
|
||
renderItem={renderItem}
|
||
numColumns={2}
|
||
columnWrapperStyle={{ justifyContent: 'space-between', marginBottom: 20 }}
|
||
contentContainerStyle={{ paddingBottom: 60 }}
|
||
onEndReached={() => hasNextPage && !isFetchingNextPage && fetchNextPage()}
|
||
onEndReachedThreshold={0.4}
|
||
ListFooterComponent={
|
||
isFetchingNextPage ? (
|
||
<View style={styles.footerLoader}>
|
||
<ActivityIndicator size="small" color="#3b82f6" />
|
||
</View>
|
||
) : null
|
||
}
|
||
ListEmptyComponent={
|
||
data ? (
|
||
<View style={styles.center}>
|
||
<Text style={{ color: '#94a3b8' }}>{t('Natija topilmadi')}</Text>
|
||
</View>
|
||
) : null
|
||
}
|
||
/>
|
||
|
||
<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}
|
||
>
|
||
{/* Loading holati */}
|
||
{loadingDetail && (
|
||
<View style={styles.center}>
|
||
<ActivityIndicator size="large" color="#3b82f6" />
|
||
</View>
|
||
)}
|
||
|
||
{/* Error holati */}
|
||
{detailError && (
|
||
<View style={styles.center}>
|
||
<Text style={{ color: '#ef4444', fontWeight: '600' }}>
|
||
{t('Xatolik yuz berdi')}
|
||
</Text>
|
||
</View>
|
||
)}
|
||
|
||
{/* Detail mavjud bo‘lsa */}
|
||
{detail && (
|
||
<>
|
||
<View style={styles.carouselWrapper}>
|
||
<FlatList
|
||
nestedScrollEnabled={true}
|
||
data={detail.files || []}
|
||
renderItem={renderCarouselItem}
|
||
keyExtractor={(item) => item.id.toString()}
|
||
horizontal
|
||
pagingEnabled
|
||
showsHorizontalScrollIndicator={false}
|
||
onMomentumScrollEnd={(e) => {
|
||
const index = Math.round(e.nativeEvent.contentOffset.x / IMAGE_WIDTH);
|
||
setCurrentImageIndex(index);
|
||
}}
|
||
/>
|
||
{detail.files.length > 1 && (
|
||
<View style={styles.pagination}>
|
||
{detail.files.map((_, i) => (
|
||
<View
|
||
key={i}
|
||
style={[
|
||
styles.paginationDot,
|
||
currentImageIndex === i && styles.paginationDotActive,
|
||
]}
|
||
/>
|
||
))}
|
||
</View>
|
||
)}
|
||
</View>
|
||
|
||
<View style={styles.sheetHeader}>
|
||
<Text style={[styles.sheetTitle, isDark ? styles.darkText : styles.lightText]}>
|
||
{detail.title}
|
||
</Text>
|
||
<View style={styles.sheetCompanyBadge}>
|
||
<Text style={styles.sheetCompanyText}>{detail.company}</Text>
|
||
</View>
|
||
</View>
|
||
|
||
<View style={[styles.divider, isDark ? styles.darkDivider : styles.lightDivider]} />
|
||
|
||
<View style={styles.section}>
|
||
<View style={styles.sectionTitleRow}>
|
||
<Info size={18} color={isDark ? '#f1f5f9' : '#0f172a'} />
|
||
<Text style={[styles.sectionLabel, isDark ? styles.darkText : styles.lightText]}>
|
||
{t("Batafsil ma'lumot")}
|
||
</Text>
|
||
</View>
|
||
<Text style={[styles.sheetDescription, isDark ? styles.darkText : styles.lightText]}>
|
||
{detail.description || "Ma'lumot mavjud emas."}
|
||
</Text>
|
||
</View>
|
||
</>
|
||
)}
|
||
</BottomSheetScrollView>
|
||
</BottomSheetModal>
|
||
</>
|
||
);
|
||
}
|
||
|
||
const styles = StyleSheet.create({
|
||
listContainer: { gap: 0, paddingBottom: 20 },
|
||
card: {
|
||
borderRadius: 16,
|
||
overflow: 'hidden',
|
||
width: (SCREEN_WIDTH - 40) / 2,
|
||
marginLeft: 2,
|
||
shadowColor: '#000',
|
||
shadowOpacity: 0.15,
|
||
shadowOffset: { width: 0, height: 4 },
|
||
shadowRadius: 50,
|
||
elevation: 4,
|
||
},
|
||
darkCard: {
|
||
backgroundColor: '#1e293b',
|
||
},
|
||
lightCard: {
|
||
backgroundColor: '#ffffff',
|
||
},
|
||
imageContainer: {
|
||
width: '100%',
|
||
height: 160,
|
||
},
|
||
darkImageBg: {
|
||
backgroundColor: '#0f172a',
|
||
},
|
||
lightImageBg: {
|
||
backgroundColor: '#f1f5f9',
|
||
},
|
||
image: { width: '100%', height: '100%' },
|
||
videoIconOverlay: {
|
||
position: 'absolute',
|
||
top: 0,
|
||
left: 0,
|
||
right: 0,
|
||
bottom: 0,
|
||
justifyContent: 'center',
|
||
alignItems: 'center',
|
||
backgroundColor: 'rgba(0,0,0,0.25)',
|
||
},
|
||
placeholder: { flex: 1, alignItems: 'center', justifyContent: 'center' },
|
||
info: { padding: 12 },
|
||
title: {
|
||
fontSize: 16,
|
||
fontWeight: '700',
|
||
marginBottom: 4,
|
||
},
|
||
darkText: {
|
||
color: '#f1f5f9',
|
||
},
|
||
lightText: {
|
||
color: '#0f172a',
|
||
},
|
||
companyRow: { flexDirection: 'row', alignItems: 'center', gap: 6 },
|
||
companyText: {
|
||
fontSize: 13,
|
||
},
|
||
darkSubText: {
|
||
color: '#64748b',
|
||
},
|
||
lightSubText: {
|
||
color: '#94a3b8',
|
||
},
|
||
|
||
// Bottom Sheet
|
||
sheetContent: { flex: 1 },
|
||
sheetContentContainer: { paddingHorizontal: 20, paddingBottom: 40 },
|
||
carouselWrapper: {
|
||
width: IMAGE_WIDTH,
|
||
height: 280,
|
||
marginBottom: 20,
|
||
borderRadius: 16,
|
||
overflow: 'hidden',
|
||
alignSelf: 'center',
|
||
},
|
||
carouselImageContainer: { width: IMAGE_WIDTH, height: 280, backgroundColor: '#e2e8f0' },
|
||
carouselImage: { width: '100%', height: '100%' },
|
||
pagination: {
|
||
position: 'absolute',
|
||
bottom: 12,
|
||
flexDirection: 'row',
|
||
width: '100%',
|
||
justifyContent: 'center',
|
||
alignItems: 'center',
|
||
gap: 8,
|
||
},
|
||
paginationDot: { width: 8, height: 8, borderRadius: 4, backgroundColor: 'rgba(255,255,255,0.4)' },
|
||
paginationDotActive: { backgroundColor: '#3b82f6', width: 20 },
|
||
|
||
sheetHeader: { marginBottom: 16 },
|
||
sheetTitle: {
|
||
fontSize: 20,
|
||
fontWeight: '800',
|
||
marginBottom: 8,
|
||
},
|
||
sheetCompanyBadge: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: 6,
|
||
backgroundColor: '#3b82f6',
|
||
alignSelf: 'flex-start',
|
||
paddingHorizontal: 10,
|
||
paddingVertical: 5,
|
||
borderRadius: 12,
|
||
},
|
||
sheetCompanyText: { fontSize: 13, color: '#ffffff', fontWeight: '700' },
|
||
divider: {
|
||
height: 1,
|
||
marginBottom: 20,
|
||
},
|
||
darkDivider: {
|
||
backgroundColor: '#334155',
|
||
},
|
||
lightDivider: {
|
||
backgroundColor: '#e2e8f0',
|
||
},
|
||
section: { marginBottom: 20 },
|
||
sectionTitleRow: { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 8 },
|
||
sectionLabel: {
|
||
fontSize: 16,
|
||
fontWeight: '700',
|
||
},
|
||
sheetDescription: {
|
||
fontSize: 15,
|
||
lineHeight: 22,
|
||
},
|
||
|
||
center: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||
footerLoader: { paddingVertical: 20, alignItems: 'center' },
|
||
});
|