diff --git a/mobile/app/index.tsx b/mobile/app/index.tsx index 3f1abec..0d26bd7 100644 --- a/mobile/app/index.tsx +++ b/mobile/app/index.tsx @@ -1,10 +1,11 @@ import React, { useState, useMemo } from 'react'; -import { View, FlatList, StyleSheet, TouchableOpacity, Text, Dimensions, Image, ActivityIndicator, TextInput } from 'react-native'; +import { View, FlatList, StyleSheet, TouchableOpacity, Text, Dimensions, Image, ActivityIndicator } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { MaterialIcons } from '@expo/vector-icons'; import { useFiles, useFileImage } from '../hooks/useFiles'; import { FileItem } from '../types'; +import { SearchBar, SearchFilters } from '../components/SearchBar'; const NUM_COLUMNS = 3; const SCREEN_WIDTH = Dimensions.get('window').width; @@ -75,15 +76,23 @@ function FileGridItem({ file, onPress }: { file: FileItem; onPress?: () => void ); } -function matchesQuery(file: FileItem, query: string): boolean { +function matchesQuery(file: FileItem, query: string, filters: SearchFilters): boolean { if (!query) return true; const q = query.toLowerCase(); - if (file.name.toLowerCase().includes(q)) return true; - if (file.ocrText?.toLowerCase().includes(q)) return true; - if (file.tags?.some((t) => { + if (filters.name && file.name.toLowerCase().includes(q)) return true; + if (filters.ocrText && file.ocrText?.toLowerCase().includes(q)) return true; + if (filters.tags && file.tags?.some((t) => { const tagName = typeof t === 'string' ? t : t.name; return tagName?.toLowerCase().includes(q); })) return true; + if (!filters.name && !filters.ocrText && !filters.tags) { + if (file.name.toLowerCase().includes(q)) return true; + if (file.ocrText?.toLowerCase().includes(q)) return true; + if (file.tags?.some((t) => { + const tagName = typeof t === 'string' ? t : t.name; + return tagName?.toLowerCase().includes(q); + })) return true; + } return false; } @@ -91,12 +100,13 @@ export function HomeScreen() { const navigation = useNavigation(); const { data, isLoading, error } = useFiles(); const [searchQuery, setSearchQuery] = useState(''); + const [filters, setFilters] = useState({ name: true, ocrText: true, tags: true }); const files = data?.data ?? []; const filteredFiles = useMemo( - () => searchQuery.trim() ? files.filter((f) => matchesQuery(f, searchQuery)) : files, - [files, searchQuery] + () => searchQuery.trim() ? files.filter((f) => matchesQuery(f, searchQuery, filters)) : files, + [files, searchQuery, filters] ); const groups = groupByDate(filteredFiles); @@ -126,23 +136,6 @@ export function HomeScreen() { return ( - - - {searchQuery.length > 0 && ( - setSearchQuery('')} style={styles.clearBtn}> - - - )} - - item} @@ -178,6 +171,14 @@ export function HomeScreen() { }} /> + setSearchQuery('')} + filters={filters} + onFiltersChange={setFilters} + /> + {}}> @@ -218,31 +219,6 @@ const styles = StyleSheet.create({ fontSize: 16, color: '#666', }, - searchBar: { - flexDirection: 'row', - alignItems: 'center', - paddingHorizontal: 16, - paddingTop: 12, - paddingBottom: 8, - }, - searchInput: { - flex: 1, - backgroundColor: '#fff', - borderRadius: 10, - paddingHorizontal: 14, - paddingVertical: 10, - fontSize: 15, - borderWidth: 1, - borderColor: '#e0e0e0', - }, - clearBtn: { - marginLeft: 8, - padding: 6, - }, - clearText: { - fontSize: 18, - color: '#999', - }, list: { padding: 16, paddingBottom: 80, diff --git a/mobile/components/SearchBar.tsx b/mobile/components/SearchBar.tsx new file mode 100644 index 0000000..2b79190 --- /dev/null +++ b/mobile/components/SearchBar.tsx @@ -0,0 +1,180 @@ +import React, { useRef, useEffect } from 'react'; +import { View, TextInput, TouchableOpacity, Text, StyleSheet, Animated } from 'react-native'; +import { MaterialIcons } from '@expo/vector-icons'; +import type { ComponentProps } from 'react'; + +type IconName = ComponentProps['name']; + +export interface SearchFilters { + name: boolean; + ocrText: boolean; + tags: boolean; +} + +interface SearchBarProps { + query: string; + onQueryChange: (q: string) => void; + onClear: () => void; + filters: SearchFilters; + onFiltersChange: (f: SearchFilters) => void; +} + +const FILTER_OPTIONS: { key: keyof SearchFilters; label: string; icon: IconName }[] = [ + { key: 'name', label: 'Nom', icon: 'drive-file-rename-outline' }, + { key: 'ocrText', label: 'Texte OCR', icon: 'document-scanner' }, + { key: 'tags', label: 'Tags', icon: 'label-outline' }, +]; + +export function SearchBar({ query, onQueryChange, onClear, filters, onFiltersChange }: SearchBarProps) { + const animatedHeight = useRef(new Animated.Value(0)).current; + const [panelOpen, setPanelOpen] = React.useState(false); + + useEffect(() => { + Animated.spring(animatedHeight, { + toValue: panelOpen ? 1 : 0, + useNativeDriver: false, + tension: 60, + friction: 8, + }).start(); + }, [panelOpen]); + + const panelMaxHeight = animatedHeight.interpolate({ + inputRange: [0, 1], + outputRange: [0, 100], + }); + + const toggleFilter = (key: keyof SearchFilters) => { + onFiltersChange({ ...filters, [key]: !filters[key] }); + }; + + const hasActiveFilter = filters.name || filters.ocrText || filters.tags; + + return ( + + + + + + {query.length > 0 && ( + + + + )} + + + setPanelOpen(!panelOpen)} + > + + + + + + {FILTER_OPTIONS.map((opt) => ( + toggleFilter(opt.key)} + > + + + {opt.label} + + + ))} + + + ); +} + +const styles = StyleSheet.create({ + wrapper: { + backgroundColor: '#fff', + borderTopWidth: 1, + borderTopColor: '#e0e0e0', + }, + inputRow: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 12, + paddingVertical: 10, + gap: 8, + }, + inputContainer: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + backgroundColor: '#f5f5f5', + borderRadius: 10, + paddingHorizontal: 10, + }, + searchIcon: { + marginRight: 6, + }, + input: { + flex: 1, + paddingVertical: 8, + fontSize: 14, + color: '#333', + }, + clearBtn: { + padding: 4, + }, + filterBtn: { + width: 40, + height: 40, + borderRadius: 10, + borderWidth: 1, + borderColor: '#1976D2', + justifyContent: 'center', + alignItems: 'center', + backgroundColor: '#fff', + }, + filterBtnActive: { + backgroundColor: '#1976D2', + }, + filterPanel: { + flexDirection: 'row', + paddingHorizontal: 12, + paddingBottom: 10, + gap: 8, + overflow: 'hidden', + }, + filterChip: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 16, + borderWidth: 1, + borderColor: '#1976D2', + gap: 4, + }, + filterChipActive: { + backgroundColor: '#1976D2', + }, + filterChipText: { + fontSize: 12, + color: '#1976D2', + }, + filterChipTextActive: { + color: '#fff', + }, +});