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 { 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() {
|
export default function SearchSection() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<View style={styles.searchWrapper}>
|
<SearchBar placeholder={t("home.searchPlaceholder") ?? "Rechercher..."} />
|
||||||
{/* 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>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -43,38 +19,4 @@ const styles = StyleSheet.create({
|
||||||
paddingBottom: 16,
|
paddingBottom: 16,
|
||||||
paddingTop: 4,
|
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 { useState } from "react";
|
||||||
import {
|
import { View, ScrollView, Pressable, StyleSheet } from "react-native";
|
||||||
View,
|
|
||||||
TextInput,
|
|
||||||
ScrollView,
|
|
||||||
Pressable,
|
|
||||||
StyleSheet,
|
|
||||||
Image,
|
|
||||||
} from "react-native";
|
|
||||||
import { useTranslation } from "react-i18next";
|
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 { Text } from "@/components/ui/text";
|
||||||
|
import SearchBar from "@/components/shared/SearchBar";
|
||||||
import { HeaderActionButtons } from "@/components/shared/HeaderActionButtons";
|
import { HeaderActionButtons } from "@/components/shared/HeaderActionButtons";
|
||||||
import { colors } from "@/theme/colors";
|
import { colors } from "@/theme/colors";
|
||||||
import { WINE_REGIONS } from "@/data/wineRegions";
|
import { WINE_REGIONS } from "@/data/wineRegions";
|
||||||
|
|
@ -43,24 +37,14 @@ export function FloatingSearch({
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.root} collapsable={false}>
|
<View collapsable={false} style={styles.rootElevation}>
|
||||||
<View style={styles.searchRow}>
|
<View className="flex-row items-center gap-2.5">
|
||||||
<View style={styles.searchBar}>
|
<View className="flex-1">
|
||||||
<Search size={20} color={colors.primary[800]} strokeWidth={2} />
|
<SearchBar
|
||||||
<TextInput
|
placeholder={t("map.searchPlaceholder")}
|
||||||
value={query}
|
value={query}
|
||||||
onChangeText={setQuery}
|
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>
|
</View>
|
||||||
<HeaderActionButtons />
|
<HeaderActionButtons />
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -68,7 +52,7 @@ export function FloatingSearch({
|
||||||
<ScrollView
|
<ScrollView
|
||||||
horizontal
|
horizontal
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
contentContainerStyle={styles.chipsRow}
|
contentContainerClassName="gap-2 pt-3 px-0.5 py-1"
|
||||||
>
|
>
|
||||||
{filters.map((filter) => {
|
{filters.map((filter) => {
|
||||||
const isActive = activeFilter === filter.id;
|
const isActive = activeFilter === filter.id;
|
||||||
|
|
@ -76,17 +60,36 @@ export function FloatingSearch({
|
||||||
<Pressable
|
<Pressable
|
||||||
key={filter.id}
|
key={filter.id}
|
||||||
onPress={() => onFilterPress?.(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" && (
|
{filter.icon === "location" && (
|
||||||
<MapPin
|
<MapPin
|
||||||
size={14}
|
size={16}
|
||||||
color={isActive ? "#FFFFFF" : colors.neutral[800]}
|
color={isActive ? "#FFFFFF" : colors.primary[800]}
|
||||||
strokeWidth={2.2}
|
strokeWidth={2.2}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Text
|
<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)}
|
{t(filter.labelKey)}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
@ -99,83 +102,13 @@ export function FloatingSearch({
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
root: {
|
rootElevation: {
|
||||||
elevation: 24,
|
elevation: 24,
|
||||||
},
|
},
|
||||||
searchRow: {
|
chipShadow: {
|
||||||
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",
|
|
||||||
shadowColor: "#000",
|
shadowColor: "#000",
|
||||||
shadowOffset: { width: 0, height: 2 },
|
shadowOffset: { width: 0, height: 2 },
|
||||||
shadowOpacity: 0.04,
|
shadowOpacity: 0.04,
|
||||||
shadowRadius: 6,
|
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 {
|
import {
|
||||||
View,
|
View,
|
||||||
FlatList,
|
FlatList,
|
||||||
TextInput,
|
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
Alert,
|
Alert,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
|
|
@ -12,12 +11,12 @@ import { useNavigation, useFocusEffect } from '@react-navigation/native';
|
||||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Image } from 'expo-image';
|
import { Image } from 'expo-image';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { ScanLine } from 'lucide-react-native';
|
||||||
import { Search, ScanLine } from 'lucide-react-native';
|
|
||||||
|
|
||||||
import { Text } from '@/components/ui/text';
|
import { Text } from '@/components/ui/text';
|
||||||
import { DateGroupAccordion } from '@/components/my-plants/DateGroupAccordion';
|
import { DateGroupAccordion } from '@/components/my-plants/DateGroupAccordion';
|
||||||
import { HeaderActionButtons } from '@/components/shared/HeaderActionButtons';
|
import { HeaderActionButtons } from '@/components/shared/HeaderActionButtons';
|
||||||
|
import SearchBar from '@/components/shared/SearchBar';
|
||||||
import { useHistory } from '@/hooks/useHistory';
|
import { useHistory } from '@/hooks/useHistory';
|
||||||
import { getCepageById } from '@/utils/cepages';
|
import { getCepageById } from '@/utils/cepages';
|
||||||
import { groupScansByDate } from '@/utils/dateGrouping';
|
import { groupScansByDate } from '@/utils/dateGrouping';
|
||||||
|
|
@ -142,27 +141,11 @@ export default function MyPlantsScreen() {
|
||||||
|
|
||||||
{/* Search bar */}
|
{/* Search bar */}
|
||||||
<View style={styles.searchContainer}>
|
<View style={styles.searchContainer}>
|
||||||
<View style={styles.searchWrapper}>
|
<SearchBar
|
||||||
<Search size={20} color={colors.neutral[400]} />
|
placeholder={t('myPlants.searchPlaceholder')}
|
||||||
<TextInput
|
value={searchQuery}
|
||||||
style={styles.searchInput}
|
onChangeText={setSearchQuery}
|
||||||
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>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
|
|
@ -225,28 +208,6 @@ const styles = StyleSheet.create({
|
||||||
paddingHorizontal: 20,
|
paddingHorizontal: 20,
|
||||||
paddingVertical: 12,
|
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
|
// List
|
||||||
listContent: {
|
listContent: {
|
||||||
paddingBottom: 100,
|
paddingBottom: 100,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue