feat(ui): shared SearchBar reused on Home, Map and MyPlants
Extrait la barre de recherche en composant partagé
(components/shared/SearchBar.tsx) avec props placeholder/value/onChangeText/
showFilter.
- Home (SearchSection) : utilise le composant partagé
- Map (FloatingSearch) : remplace l'input custom + ajuste les chips (border
primary, font-size 15→12, MapPin couleur primary)
- MyPlants : remplace l'input custom + son bouton clear
Bonus : SearchBar gère proprement le clavier Android via numberOfLines={1},
multiline={false}, scrollEnabled={false}, lineHeight 20 + textAlignVertical
center + includeFontPadding false → le placeholder ne wrappe plus sur 2 lignes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c2b04757a4
commit
3781b1c0f4
|
|
@ -1,38 +1,14 @@
|
|||
import { View, TextInput, StyleSheet, TouchableOpacity } from "react-native";
|
||||
import { View, StyleSheet } from "react-native";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
|
||||
import { colors } from "@/theme/colors";
|
||||
import SearchBar from "@/components/shared/SearchBar";
|
||||
|
||||
export default function SearchSection() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.searchWrapper}>
|
||||
{/* Icône de recherche */}
|
||||
<Ionicons
|
||||
name="search"
|
||||
size={20}
|
||||
color={colors.neutral[400]}
|
||||
style={styles.searchIcon}
|
||||
/>
|
||||
|
||||
{/* Champ de saisie */}
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder={t("home.searchPlaceholder") ?? "Rechercher..."}
|
||||
placeholderTextColor={colors.neutral[400]}
|
||||
selectionColor={colors.primary[500]}
|
||||
autoCorrect={false}
|
||||
/>
|
||||
|
||||
{/* Optionnel: Petit séparateur + Icône Filtre pour le look Premium */}
|
||||
<TouchableOpacity style={styles.filterButton} activeOpacity={0.7}>
|
||||
<View style={styles.divider} />
|
||||
<Ionicons name="options-outline" size={18} color={colors.primary[600]} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<SearchBar placeholder={t("home.searchPlaceholder") ?? "Rechercher..."} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
@ -43,38 +19,4 @@ const styles = StyleSheet.create({
|
|||
paddingBottom: 16,
|
||||
paddingTop: 4,
|
||||
},
|
||||
searchWrapper: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
backgroundColor: "#F5F7F9", // Un gris bleuté plus frais que neutral-200
|
||||
borderRadius: 100, // On garde ton style "full"
|
||||
paddingHorizontal: 16,
|
||||
height: 52, // Hauteur standardisée pour le tactile
|
||||
borderWidth: 1,
|
||||
borderColor: "#EAECEF",
|
||||
},
|
||||
searchIcon: {
|
||||
marginRight: 10,
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
fontSize: 15,
|
||||
|
||||
fontWeight: "500",
|
||||
color: colors.neutral[900],
|
||||
// Évite le décalage de texte sur Android
|
||||
paddingVertical: 0,
|
||||
height: "100%",
|
||||
},
|
||||
filterButton: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingLeft: 12,
|
||||
},
|
||||
divider: {
|
||||
width: 1,
|
||||
height: 20,
|
||||
backgroundColor: "#E2E4E7",
|
||||
marginRight: 12,
|
||||
},
|
||||
});
|
||||
|
|
@ -1,16 +1,10 @@
|
|||
import { useState } from "react";
|
||||
import {
|
||||
View,
|
||||
TextInput,
|
||||
ScrollView,
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
Image,
|
||||
} from "react-native";
|
||||
import { View, ScrollView, Pressable, StyleSheet } from "react-native";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Search, MapPin } from "lucide-react-native";
|
||||
import { MapPin } from "lucide-react-native";
|
||||
|
||||
import { Text } from "@/components/ui/text";
|
||||
import SearchBar from "@/components/shared/SearchBar";
|
||||
import { HeaderActionButtons } from "@/components/shared/HeaderActionButtons";
|
||||
import { colors } from "@/theme/colors";
|
||||
import { WINE_REGIONS } from "@/data/wineRegions";
|
||||
|
|
@ -43,24 +37,14 @@ export function FloatingSearch({
|
|||
];
|
||||
|
||||
return (
|
||||
<View style={styles.root} collapsable={false}>
|
||||
<View style={styles.searchRow}>
|
||||
<View style={styles.searchBar}>
|
||||
<Search size={20} color={colors.primary[800]} strokeWidth={2} />
|
||||
<TextInput
|
||||
<View collapsable={false} style={styles.rootElevation}>
|
||||
<View className="flex-row items-center gap-2.5">
|
||||
<View className="flex-1">
|
||||
<SearchBar
|
||||
placeholder={t("map.searchPlaceholder")}
|
||||
value={query}
|
||||
onChangeText={setQuery}
|
||||
placeholder={t("map.searchPlaceholder")}
|
||||
placeholderTextColor={colors.neutral[500]}
|
||||
style={styles.input}
|
||||
/>
|
||||
{/* <View style={styles.logoWrap}>
|
||||
<Image
|
||||
source={require("../../../assets/logo.png")}
|
||||
style={styles.logo}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
</View> */}
|
||||
</View>
|
||||
<HeaderActionButtons />
|
||||
</View>
|
||||
|
|
@ -68,7 +52,7 @@ export function FloatingSearch({
|
|||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.chipsRow}
|
||||
contentContainerClassName="gap-2 pt-3 px-0.5 py-1"
|
||||
>
|
||||
{filters.map((filter) => {
|
||||
const isActive = activeFilter === filter.id;
|
||||
|
|
@ -76,17 +60,36 @@ export function FloatingSearch({
|
|||
<Pressable
|
||||
key={filter.id}
|
||||
onPress={() => onFilterPress?.(filter.id)}
|
||||
style={[styles.chip, isActive && styles.chipActive]}
|
||||
className="flex-row items-center gap-1.5 px-3 py-1.5 rounded-full border"
|
||||
style={[
|
||||
styles.chipShadow,
|
||||
isActive
|
||||
? {
|
||||
backgroundColor: colors.primary[800],
|
||||
borderColor: colors.primary[800],
|
||||
shadowOpacity: 0.12,
|
||||
// elevation: 24,
|
||||
}
|
||||
: {
|
||||
backgroundColor: "#FFFFFF",
|
||||
borderColor: colors.primary[800],
|
||||
},
|
||||
]}
|
||||
>
|
||||
{filter.icon === "location" && (
|
||||
<MapPin
|
||||
size={14}
|
||||
color={isActive ? "#FFFFFF" : colors.neutral[800]}
|
||||
size={16}
|
||||
color={isActive ? "#FFFFFF" : colors.primary[800]}
|
||||
strokeWidth={2.2}
|
||||
/>
|
||||
)}
|
||||
<Text
|
||||
style={[styles.chipText, isActive && styles.chipTextActive]}
|
||||
className={
|
||||
isActive
|
||||
? "text-[12px] font-semibold text-white"
|
||||
: "text-[12px] font-medium"
|
||||
}
|
||||
style={!isActive ? { color: colors.primary[800] } : undefined}
|
||||
>
|
||||
{t(filter.labelKey)}
|
||||
</Text>
|
||||
|
|
@ -99,83 +102,13 @@ export function FloatingSearch({
|
|||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
root: {
|
||||
rootElevation: {
|
||||
elevation: 24,
|
||||
},
|
||||
searchRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
},
|
||||
searchBar: {
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
backgroundColor: "#FFFFFF",
|
||||
borderRadius: 75,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
gap: 12,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.12,
|
||||
shadowRadius: 12,
|
||||
elevation: 24,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.neutral[200],
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
fontSize: 14,
|
||||
color: colors.neutral[900],
|
||||
padding: 0,
|
||||
},
|
||||
logoWrap: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
overflow: "hidden",
|
||||
borderWidth: 2,
|
||||
borderColor: colors.primary[200],
|
||||
},
|
||||
logo: {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
},
|
||||
chipsRow: {
|
||||
gap: 8,
|
||||
paddingTop: 12,
|
||||
paddingHorizontal: 2,
|
||||
},
|
||||
chip: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
backgroundColor: "#FFFFFF",
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 999,
|
||||
borderWidth: 1,
|
||||
borderColor: "#F0F0F0",
|
||||
chipShadow: {
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.04,
|
||||
shadowRadius: 6,
|
||||
elevation: 24,
|
||||
},
|
||||
chipActive: {
|
||||
backgroundColor: colors.primary[800],
|
||||
borderColor: colors.primary[800],
|
||||
shadowOpacity: 0.12,
|
||||
elevation: 24,
|
||||
},
|
||||
chipText: {
|
||||
fontSize: 13,
|
||||
fontWeight: "500",
|
||||
color: "#2D2D2D",
|
||||
},
|
||||
chipTextActive: {
|
||||
color: "#FFFFFF",
|
||||
fontWeight: "600",
|
||||
},
|
||||
});
|
||||
|
|
|
|||
97
VinEye/src/components/shared/SearchBar.tsx
Normal file
97
VinEye/src/components/shared/SearchBar.tsx
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import { View, TextInput, TouchableOpacity, StyleSheet } from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
|
||||
import { colors } from "@/theme/colors";
|
||||
|
||||
interface SearchBarProps {
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
onChangeText?: (text: string) => void;
|
||||
onFilterPress?: () => void;
|
||||
showFilter?: boolean;
|
||||
}
|
||||
|
||||
export default function SearchBar({
|
||||
placeholder,
|
||||
value,
|
||||
onChangeText,
|
||||
onFilterPress,
|
||||
showFilter = true,
|
||||
}: SearchBarProps) {
|
||||
return (
|
||||
<View style={styles.searchWrapper}>
|
||||
<Ionicons
|
||||
name="search"
|
||||
size={20}
|
||||
color={colors.neutral[400]}
|
||||
style={styles.searchIcon}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={value}
|
||||
multiline={false}
|
||||
numberOfLines={1}
|
||||
scrollEnabled={false}
|
||||
onChangeText={onChangeText}
|
||||
placeholder={placeholder ?? "Rechercher..."}
|
||||
placeholderTextColor={colors.neutral[400]}
|
||||
selectionColor={colors.primary[500]}
|
||||
autoCorrect={false}
|
||||
/>
|
||||
|
||||
{showFilter && (
|
||||
<TouchableOpacity
|
||||
onPress={onFilterPress}
|
||||
style={styles.filterButton}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={styles.divider} />
|
||||
<Ionicons
|
||||
name="options-outline"
|
||||
size={18}
|
||||
color={colors.primary[600]}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
searchWrapper: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
backgroundColor: "#fff",
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: 16,
|
||||
height: 52,
|
||||
borderWidth: 1,
|
||||
borderColor: "#EAECEF",
|
||||
},
|
||||
searchIcon: {
|
||||
marginRight: 10,
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
fontSize: 15,
|
||||
lineHeight: 20,
|
||||
fontWeight: "500",
|
||||
color: colors.neutral[900],
|
||||
paddingVertical: 0,
|
||||
paddingHorizontal: 0,
|
||||
textAlignVertical: "center",
|
||||
includeFontPadding: false,
|
||||
},
|
||||
filterButton: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingLeft: 12,
|
||||
},
|
||||
divider: {
|
||||
width: 1,
|
||||
height: 20,
|
||||
backgroundColor: "#E2E4E7",
|
||||
marginRight: 12,
|
||||
},
|
||||
});
|
||||
|
|
@ -2,7 +2,6 @@ import { useState, useMemo, useCallback } from 'react';
|
|||
import {
|
||||
View,
|
||||
FlatList,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
Alert,
|
||||
StyleSheet,
|
||||
|
|
@ -12,12 +11,12 @@ import { useNavigation, useFocusEffect } from '@react-navigation/native';
|
|||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Image } from 'expo-image';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { Search, ScanLine } from 'lucide-react-native';
|
||||
import { ScanLine } from 'lucide-react-native';
|
||||
|
||||
import { Text } from '@/components/ui/text';
|
||||
import { DateGroupAccordion } from '@/components/my-plants/DateGroupAccordion';
|
||||
import { HeaderActionButtons } from '@/components/shared/HeaderActionButtons';
|
||||
import SearchBar from '@/components/shared/SearchBar';
|
||||
import { useHistory } from '@/hooks/useHistory';
|
||||
import { getCepageById } from '@/utils/cepages';
|
||||
import { groupScansByDate } from '@/utils/dateGrouping';
|
||||
|
|
@ -142,27 +141,11 @@ export default function MyPlantsScreen() {
|
|||
|
||||
{/* Search bar */}
|
||||
<View style={styles.searchContainer}>
|
||||
<View style={styles.searchWrapper}>
|
||||
<Search size={20} color={colors.neutral[400]} />
|
||||
<TextInput
|
||||
style={styles.searchInput}
|
||||
placeholder={t('myPlants.searchPlaceholder')}
|
||||
placeholderTextColor={colors.neutral[400]}
|
||||
selectionColor={colors.primary[500]}
|
||||
autoCorrect={false}
|
||||
value={searchQuery}
|
||||
onChangeText={setSearchQuery}
|
||||
/>
|
||||
{searchQuery.length > 0 && (
|
||||
<TouchableOpacity
|
||||
onPress={() => setSearchQuery('')}
|
||||
style={styles.clearBtn}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Ionicons name="close-circle" size={18} color={colors.neutral[400]} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
<SearchBar
|
||||
placeholder={t('myPlants.searchPlaceholder')}
|
||||
value={searchQuery}
|
||||
onChangeText={setSearchQuery}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Content */}
|
||||
|
|
@ -225,28 +208,6 @@ const styles = StyleSheet.create({
|
|||
paddingHorizontal: 20,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
searchWrapper: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#F5F7F9',
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: 16,
|
||||
height: 48,
|
||||
borderWidth: 1,
|
||||
borderColor: '#EAECEF',
|
||||
gap: 10,
|
||||
},
|
||||
searchInput: {
|
||||
flex: 1,
|
||||
fontSize: 15,
|
||||
fontWeight: '500',
|
||||
color: colors.neutral[900],
|
||||
paddingVertical: 0,
|
||||
height: '100%',
|
||||
},
|
||||
clearBtn: {
|
||||
padding: 4,
|
||||
},
|
||||
// List
|
||||
listContent: {
|
||||
paddingBottom: 100,
|
||||
|
|
|
|||
Loading…
Reference in a new issue