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

672 lines
18 KiB
TypeScript

import { useTheme } from '@/components/ThemeContext';
import { products_api } from '@/screens/home/lib/api';
import BottomSheet, {
BottomSheetBackdrop,
BottomSheetFlatList,
BottomSheetScrollView,
} from '@gorhom/bottom-sheet';
import { useMutation, useQuery } from '@tanstack/react-query';
import { AxiosError } from 'axios';
import { Image } from 'expo-image';
import { CheckIcon, ChevronRight, XIcon } from 'lucide-react-native';
import React, { Dispatch, SetStateAction, useCallback, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ActivityIndicator, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import CategorySelect from './CategorySelect';
interface FilterUIProps {
back: () => void;
onApply?: (data: any) => void;
setStep: (value: 'filter' | 'items') => void;
setFiltered: Dispatch<SetStateAction<any[]>>;
}
interface Category {
id: number;
name: string;
code: string;
external_id: string | null;
level: number;
is_leaf: boolean;
icon_name: string | null;
}
interface Country {
id: number;
name: string;
region: Region[];
}
interface Region {
id: number;
name: string;
districts: District[];
}
interface District {
id: number;
name: string;
}
type SheetType = 'country' | 'region' | 'district' | 'category' | null;
export default function FilterUI({ back, onApply, setStep, setFiltered }: FilterUIProps) {
const { isDark } = useTheme();
const { t } = useTranslation();
const bottomSheetRef = useRef<BottomSheet>(null);
const snapPoints = useMemo(() => ['60%', '85%'], []);
const [activeSheet, setActiveSheet] = useState<SheetType>(null);
const [selectedCategories, setSelectedCategories] = useState<Category | null>(null);
const [selectedCountry, setSelectedCountry] = useState<string>('all');
const [selectedRegion, setSelectedRegion] = useState<string>('all');
const [selectedDistrict, setSelectedDistrict] = useState<string>('all');
const { data: countryResponse, isLoading } = useQuery({
queryKey: ['country-detail'],
queryFn: async () => products_api.getStates(),
select: (res) => res.data?.data || [],
});
const { mutate, isPending } = useMutation({
mutationFn: (params: any) => products_api.businessAbout(params),
onSuccess: (data) => {
setStep('items');
setFiltered(data.data.data.results);
},
onError: (error: AxiosError) => console.log(error),
});
const handleApply = () => {
const countryObj = countryResponse?.find((c) => c.id?.toString() === selectedCountry);
const regionObj = countryObj?.region.find((r) => r.id?.toString() === selectedRegion);
const districtObj = regionObj?.districts.find((d) => d.id?.toString() === selectedDistrict);
mutate({
country: countryObj?.name || '',
region: regionObj?.name || '',
district: districtObj?.name || '',
types: selectedCategories ? selectedCategories.id : undefined,
});
};
const regions = useMemo(() => {
if (selectedCountry === 'all') return [];
const country = countryResponse?.find((c) => c.id?.toString() === selectedCountry);
return country?.region || [];
}, [countryResponse, selectedCountry]);
const districts = useMemo(() => {
if (selectedRegion === 'all') return [];
const region = regions.find((r) => r.id?.toString() === selectedRegion);
return region?.districts || [];
}, [regions, selectedRegion]);
const openSheet = useCallback((type: SheetType) => {
setActiveSheet(type);
setTimeout(() => {
bottomSheetRef.current?.snapToIndex(0);
}, 100);
}, []);
const closeSheet = useCallback(() => {
bottomSheetRef.current?.close();
setTimeout(() => setActiveSheet(null), 300);
}, []);
const renderBackdrop = useCallback(
(props: any) => (
<BottomSheetBackdrop
{...props}
appearsOnIndex={0}
disappearsOnIndex={-1}
opacity={0.6}
pressBehavior="close"
/>
),
[]
);
const getSelectedLabel = useCallback(
(type: SheetType) => {
switch (type) {
case 'country':
if (selectedCountry === 'all') return t('Barchasi');
return (
countryResponse?.find((c) => c.id?.toString() === selectedCountry)?.name || t('Tanlang')
);
case 'region':
if (selectedRegion === 'all') return t('Barchasi');
return regions.find((r) => r.id?.toString() === selectedRegion)?.name || t('Tanlang');
case 'district':
if (selectedDistrict === 'all') return t('Barchasi');
return districts.find((d) => d.id?.toString() === selectedDistrict)?.name || t('Tanlang');
case 'category':
return selectedCategories?.name || t('Tanlang');
default:
return t('Tanlang');
}
},
[
selectedCountry,
selectedRegion,
selectedDistrict,
selectedCategories,
countryResponse,
regions,
districts,
t,
]
);
const FilterButton = useCallback(
({
label,
value,
onPress,
disabled = false,
}: {
label: string;
value: string;
onPress: () => void;
disabled?: boolean;
}) => (
<TouchableOpacity
style={[
styles.filterBtn,
isDark ? styles.darkFilterBtn : styles.lightFilterBtn,
disabled && styles.disabledBtn,
]}
onPress={onPress}
disabled={disabled}
activeOpacity={0.7}
>
<View style={styles.filterBtnContent}>
<Text style={[styles.filterLabel, isDark ? styles.darkText : styles.lightText]}>
{label}
</Text>
<View style={styles.filterValueContainer}>
<Text
style={[
styles.filterValue,
isDark ? styles.darkValueText : styles.lightValueText,
disabled && styles.disabledText,
]}
numberOfLines={1}
>
{value}
</Text>
<ChevronRight size={20} color={isDark ? '#94a3b8' : '#64748b'} />
</View>
</View>
</TouchableOpacity>
),
[isDark]
);
const renderListItem = useCallback(
({
item,
onSelect,
selectedId,
}: {
item: any;
onSelect: (id: string) => void;
selectedId: string;
}) => {
const isSelected = selectedId === (item.id?.toString() || 'all');
const flagCode = item.flag ? item.flag.toLowerCase() : ''; // "uz"
return (
<TouchableOpacity
style={[
styles.listItem,
isDark ? styles.darkListItem : styles.lightListItem,
isSelected && styles.selectedListItem,
]}
onPress={() => onSelect(item.id?.toString() || 'all')}
activeOpacity={0.7}
>
<View
style={{
flexDirection: 'row',
gap: 10,
alignContent: 'center',
alignItems: 'center',
}}
>
{item.flag && (
<Image
source={{ uri: `https://flagcdn.com/w320/${flagCode}.png` }}
style={{ width: 34, height: 20 }}
resizeMode="cover" // objectFit o'rniga resizeMode ishlatildi
/>
)}
<Text
style={[
styles.listItemText,
isDark ? styles.darkText : styles.lightText,
isSelected && styles.selectedListItemText,
]}
>
{item.name}
</Text>
</View>
{isSelected && (
<View style={styles.checkmark}>
<CheckIcon color={'#3b82f6'} strokeWidth={'2.5'} size={18} />
</View>
)}
</TouchableOpacity>
);
},
[isDark]
);
const renderSheetContent = useCallback(() => {
if (activeSheet === 'category') {
return (
<BottomSheetScrollView
contentContainerStyle={styles.scrollViewContent}
showsVerticalScrollIndicator={false}
bounces={true}
>
<CategorySelect
selectedCategories={selectedCategories}
setSelectedCategories={setSelectedCategories}
/>
</BottomSheetScrollView>
);
}
let data: any[] = [];
let onSelect: (id: string) => void = () => { };
let selectedId = '';
switch (activeSheet) {
case 'country':
data = [{ id: 'all', name: t('Barchasi') }, ...(countryResponse || [])];
onSelect = (id) => {
setSelectedCountry(id);
setSelectedRegion('all');
setSelectedDistrict('all');
closeSheet();
};
selectedId = selectedCountry;
break;
case 'region':
data = [{ id: 'all', name: t('Barchasi') }, ...regions];
onSelect = (id) => {
setSelectedRegion(id);
setSelectedDistrict('all');
closeSheet();
};
selectedId = selectedRegion;
break;
case 'district':
data = [{ id: 'all', name: t('Barchasi') }, ...districts];
onSelect = (id) => {
setSelectedDistrict(id);
closeSheet();
};
selectedId = selectedDistrict;
break;
}
return (
<BottomSheetFlatList
data={data}
keyExtractor={(item: any) => item.id?.toString() || 'all'}
contentContainerStyle={styles.listContainer}
showsVerticalScrollIndicator={false}
bounces={true}
overScrollMode="always"
renderItem={({ item }: { item: any }) => renderListItem({ item, onSelect, selectedId })}
/>
);
}, [
activeSheet,
selectedCategories,
countryResponse,
regions,
districts,
selectedCountry,
selectedRegion,
selectedDistrict,
t,
closeSheet,
renderListItem,
]);
const getSheetTitle = useCallback(() => {
switch (activeSheet) {
case 'country':
return t('Davlat');
case 'region':
return t('Viloyat');
case 'district':
return t('Tuman');
case 'category':
return t('Sohalar');
default:
return '';
}
}, [activeSheet, t]);
if (isLoading) {
return (
<View style={[styles.container, isDark ? styles.darkBg : styles.lightBg]}>
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#3b82f6" />
</View>
</View>
);
}
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<View style={[styles.container, isDark ? styles.darkBg : styles.lightBg]}>
{/* Header */}
<View style={[styles.header, { borderBottomColor: isDark ? '#334155' : '#e2e8f0' }]}>
<Text style={[styles.headerTitle, isDark ? styles.darkText : styles.lightText]}>
{t('Filter')}
</Text>
<TouchableOpacity
onPress={back}
style={[styles.closeBtn, isDark ? styles.darkCloseBtn : styles.lightCloseBtn]}
>
<XIcon color={isDark ? '#f1f5f9' : '#0f172a'} size={20} />
</TouchableOpacity>
</View>
{/* Filter Options */}
<View style={styles.content}>
<FilterButton
label={t('Davlat')}
value={getSelectedLabel('country')}
onPress={() => openSheet('country')}
/>
<FilterButton
label={t('Viloyat')}
value={getSelectedLabel('region')}
onPress={() => openSheet('region')}
disabled={selectedCountry === 'all' || regions.length === 0}
/>
<FilterButton
label={t('Tuman')}
value={getSelectedLabel('district')}
onPress={() => openSheet('district')}
disabled={selectedRegion === 'all' || districts.length === 0}
/>
<FilterButton
label={t('Sohalar')}
value={getSelectedLabel('category')}
onPress={() => openSheet('category')}
/>
</View>
{/* Apply Button */}
<View style={[styles.applyBtnWrapper, { borderTopColor: isDark ? '#334155' : '#e2e8f0' }]}>
<TouchableOpacity
style={[styles.applyBtn, isPending && styles.applyBtnDisabled]}
onPress={handleApply}
disabled={isPending}
activeOpacity={0.8}
>
{isPending ? (
<ActivityIndicator color="#ffffff" />
) : (
<Text style={styles.applyBtnText}>{t("Natijalarni ko'rish")}</Text>
)}
</TouchableOpacity>
</View>
{/* Bottom Sheet */}
<BottomSheet
ref={bottomSheetRef}
index={-1}
snapPoints={snapPoints}
enablePanDownToClose={true}
enableDynamicSizing={false}
enableOverDrag={false}
onClose={() => setActiveSheet(null)}
backdropComponent={renderBackdrop}
backgroundStyle={[styles.bottomSheetBackground, isDark ? styles.darkBg : styles.lightBg]}
handleIndicatorStyle={[
styles.handleIndicator,
isDark ? styles.darkHandleIndicator : styles.lightHandleIndicator,
]}
android_keyboardInputMode="adjustResize"
keyboardBehavior="interactive"
keyboardBlurBehavior="restore"
>
<View style={[styles.sheetHeader, { borderBottomColor: isDark ? '#334155' : '#e2e8f0' }]}>
<Text style={[styles.sheetTitle, isDark ? styles.darkText : styles.lightText]}>
{getSheetTitle()}
</Text>
<TouchableOpacity onPress={closeSheet} style={styles.sheetCloseBtn}>
<XIcon color={isDark ? '#94a3b8' : '#64748b'} size={20} />
</TouchableOpacity>
</View>
{renderSheetContent()}
</BottomSheet>
</View>
</GestureHandlerRootView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
darkBg: {
backgroundColor: '#0f172a',
},
lightBg: {
backgroundColor: '#f8fafc',
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 20,
paddingVertical: 16,
borderBottomWidth: 1,
},
headerTitle: {
fontSize: 20,
fontWeight: '700',
},
closeBtn: {
padding: 8,
borderRadius: 8,
borderWidth: 1,
},
darkCloseBtn: {
backgroundColor: '#1e293b',
borderColor: '#334155',
},
lightCloseBtn: {
backgroundColor: '#ffffff',
borderColor: '#e2e8f0',
},
content: {
flex: 1,
padding: 20,
gap: 12,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
filterBtn: {
padding: 16,
borderRadius: 12,
borderWidth: 1,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.05,
shadowRadius: 2,
elevation: 1,
},
darkFilterBtn: {
backgroundColor: '#1e293b',
borderColor: '#334155',
},
lightFilterBtn: {
backgroundColor: '#ffffff',
borderColor: '#e2e8f0',
},
disabledBtn: {
opacity: 0.5,
},
filterBtnContent: {
flexDirection: 'column',
gap: 8, // 4 o'rniga 8 qildim
},
filterLabel: {
fontSize: 13,
fontWeight: '600',
marginBottom: 2, // 4 o'rniga 2 qildim
},
filterValueContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
gap: 8, // ChevronRight va text orasida gap
},
filterValue: {
fontSize: 15,
fontWeight: '500',
flex: 1,
},
darkText: {
color: '#f1f5f9',
},
lightText: {
color: '#0f172a',
},
darkValueText: {
color: '#cbd5e1',
},
lightValueText: {
color: '#64748b',
},
disabledText: {
opacity: 0.5,
},
applyBtnWrapper: {
padding: 20,
borderTopWidth: 1,
},
applyBtn: {
backgroundColor: '#3b82f6',
padding: 16,
borderRadius: 12,
alignItems: 'center',
shadowColor: '#3b82f6',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 4,
elevation: 5,
},
applyBtnDisabled: {
opacity: 0.7,
},
applyBtnText: {
color: '#ffffff',
fontWeight: '700',
fontSize: 16,
},
bottomSheetBackground: {
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
shadowColor: '#000',
shadowOffset: { width: 0, height: -2 },
shadowOpacity: 0.1,
shadowRadius: 8,
elevation: 5,
},
handleIndicator: {
width: 40,
height: 4,
borderRadius: 2,
},
darkHandleIndicator: {
backgroundColor: '#475569',
},
lightHandleIndicator: {
backgroundColor: '#cbd5e1',
},
sheetHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 20,
paddingVertical: 16,
borderBottomWidth: 1,
},
sheetTitle: {
fontSize: 18,
fontWeight: '700',
},
sheetCloseBtn: {
padding: 4,
},
listContainer: {
padding: 16,
paddingBottom: 40,
},
scrollViewContent: {
padding: 16,
paddingBottom: 40,
},
listItem: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 16,
borderRadius: 12,
marginBottom: 8,
borderWidth: 1,
},
darkListItem: {
backgroundColor: '#1e293b',
borderColor: '#334155',
},
lightListItem: {
backgroundColor: '#ffffff',
borderColor: '#e2e8f0',
},
selectedListItem: {
backgroundColor: '#3b82f6',
borderColor: '#3b82f6',
},
listItemText: {
fontSize: 15,
fontWeight: '500',
},
selectedListItemText: {
color: '#ffffff',
fontWeight: '600',
},
checkmark: {
width: 24,
height: 24,
borderRadius: 12,
backgroundColor: '#ffffff',
justifyContent: 'center',
alignItems: 'center',
},
checkmarkText: {
color: '#3b82f6',
fontSize: 16,
fontWeight: '700',
},
});