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:
Yanis 2026-05-01 00:02:28 +02:00
parent c2b04757a4
commit 3781b1c0f4
4 changed files with 142 additions and 209 deletions

View file

@ -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,
},
});

View file

@ -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",
},
});

View 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,
},
});

View file

@ -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,