Files
info-target-mobile/components/ui/ProductList.tsx
Samandar Turgunboyev d747c72c8d complated
2026-02-17 10:46:57 +05:00

377 lines
11 KiB
TypeScript

import { useTheme } from '@/components/ThemeContext';
import { products_api } from '@/screens/home/lib/api';
import { ProductResponse } from '@/screens/home/lib/types';
import { BottomSheetBackdrop, BottomSheetModal, BottomSheetScrollView } from '@gorhom/bottom-sheet';
import { useInfiniteQuery } 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 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}
>
{selectedProduct && (
<>
<View style={styles.carouselWrapper}>
<FlatList
nestedScrollEnabled={true}
data={selectedProduct.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);
}}
/>
{selectedProduct.files.length > 1 && (
<View style={styles.pagination}>
{selectedProduct.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]}>
{selectedProduct.title}
</Text>
<View style={styles.sheetCompanyBadge}>
<Text style={styles.sheetCompanyText}>{selectedProduct.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]}
>
{selectedProduct.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' },
});