Files
FarmaFinder/apps/frontend-mobile/app/(tabs)/search.tsx
T
Ichitux 2f36ef685d
Run Tests on Branches / Detect Changes (push) Successful in 10s
Run Tests on Branches / Backend Tests (push) Successful in 2m12s
Run Tests on Branches / Frontend Tests (push) Has been skipped
Run Tests on Branches / Frontend Mobile Tests (push) Successful in 1m47s
Mobile App design
2026-07-09 13:33:54 +02:00

249 lines
7.4 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { View, FlatList, StyleSheet, Text, TouchableOpacity, useWindowDimensions } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { useNavigation } from 'expo-router';
import { SearchBar } from '../../components/SearchBar';
import { MedicineCard } from '../../components/MedicineCard';
import { LoadingSpinner } from '../../components/LoadingSpinner';
import { useDebounce } from '../../hooks/useDebounce';
import { useAuth } from '../../hooks/useAuth';
import { useRecentSearches } from '../../hooks/useRecentSearches';
import { searchMedicines } from '../../services/medicines';
import { useThemeContext } from '../../components/ThemeProvider';
import { spacing, borderRadius } from '../../constants/theme';
import { Medicine } from '../../types';
import { config } from '../../constants/config';
const TABLET_MIN_WIDTH = 768;
const suggestions = [
{ name: 'Paracetamol', icon: 'medical' as const },
{ name: 'Ibuprofeno', icon: 'fitness' as const },
{ name: 'Aspirina', icon: 'heart' as const },
{ name: 'Omeprazol', icon: 'bandage' as const },
];
export default function SearchScreen() {
const { width } = useWindowDimensions();
const isTablet = width >= TABLET_MIN_WIDTH;
const { isAuthenticated } = useAuth();
const { recentSearches, addSearch, removeSearch } = useRecentSearches();
const { colors } = useThemeContext();
const [query, setQuery] = useState('');
const [results, setResults] = useState<Medicine[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const debouncedQuery = useDebounce(query, config.SEARCH_DEBOUNCE_MS);
const navigation = useNavigation();
useEffect(() => {
const parent = navigation.getParent();
if (!parent) return;
const unsubscribe = parent.addListener('tabPress', () => {
setQuery('');
setResults([]);
setIsLoading(false);
setError(null);
});
return unsubscribe;
}, [navigation]);
useEffect(() => {
if (debouncedQuery.length < 2) {
setResults([]);
return;
}
const fetchResults = async () => {
setIsLoading(true);
setError(null);
try {
const data = await searchMedicines(debouncedQuery);
setResults(data);
} catch (err) {
setError('Error al buscar medicamentos');
console.error(err);
} finally {
setIsLoading(false);
}
};
fetchResults();
}, [debouncedQuery]);
const handleSearch = (searchQuery: string) => {
setQuery(searchQuery);
if (searchQuery.trim()) addSearch(searchQuery);
};
const showSuggestions = !query && !isLoading && results.length === 0;
return (
<View style={[styles.container, { backgroundColor: colors.background }]}>
<SearchBar
onSearch={handleSearch}
value={query}
onChangeText={setQuery}
/>
{showSuggestions && (
<>
<View style={[styles.suggestionsSection, isTablet && styles.suggestionsSectionTablet]}>
<Text style={[styles.sectionTitle, { color: colors.text }]}>Sugerencias</Text>
<View style={styles.suggestionsGrid}>
{suggestions.map((s) => (
<TouchableOpacity
key={s.name}
style={[styles.suggestionCard, { backgroundColor: colors.card, borderColor: colors.border }]}
onPress={() => handleSearch(s.name)}
activeOpacity={0.7}
>
<View style={[styles.suggestionIcon, { backgroundColor: colors.surfaceLow }]}>
<Ionicons name={s.icon} size={22} color={colors.primary} />
</View>
<Text style={[styles.suggestionName, { color: colors.text }]}>{s.name}</Text>
</TouchableOpacity>
))}
</View>
</View>
{isAuthenticated && recentSearches.length > 0 && (
<View style={[styles.recentSection, isTablet && styles.recentSectionTablet]}>
<Text style={[styles.sectionTitle, { color: colors.text }]}>Búsquedas recientes</Text>
{recentSearches.map((term) => (
<TouchableOpacity
key={term}
style={styles.recentItem}
onPress={() => handleSearch(term)}
activeOpacity={0.7}
>
<Ionicons name="time-outline" size={18} color={colors.textSecondary} />
<Text style={[styles.recentText, { color: colors.text }]}>{term}</Text>
<TouchableOpacity onPress={() => removeSearch(term)} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}>
<Ionicons name="close" size={16} color={colors.textSecondary} />
</TouchableOpacity>
</TouchableOpacity>
))}
</View>
)}
</>
)}
{isLoading && <LoadingSpinner message="Buscando medicamentos..." />}
{error && (
<View style={[styles.errorContainer, isTablet && styles.errorContainerTablet, { backgroundColor: colors.dangerContainer }]}>
<Text style={[styles.errorText, { color: colors.danger }]}>{error}</Text>
</View>
)}
{!isLoading && !error && results.length === 0 && query.length >= 2 && (
<View style={styles.emptyContainer}>
<Text style={[styles.emptyText, { color: colors.textSecondary }]}>No se encontraron medicamentos</Text>
</View>
)}
<FlatList
data={results}
keyExtractor={(item) => item.nregistro}
renderItem={({ item }) => <MedicineCard medicine={item} />}
contentContainerStyle={[styles.list, isTablet && styles.listTablet]}
showsVerticalScrollIndicator={false}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
suggestionsSection: {
marginTop: spacing.sm,
alignSelf: 'center',
width: '80%',
},
suggestionsSectionTablet: {
maxWidth: 700,
alignSelf: 'center',
width: '100%',
},
sectionTitle: {
fontSize: 20,
fontWeight: '700',
marginBottom: spacing.md,
},
suggestionsGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: spacing.sm,
},
suggestionCard: {
width: '48%' as unknown as number,
borderRadius: borderRadius.lg,
borderWidth: 1,
padding: spacing.md,
gap: spacing.sm,
},
suggestionIcon: {
width: 40,
height: 40,
borderRadius: borderRadius.md,
alignItems: 'center',
justifyContent: 'center',
},
suggestionName: {
fontSize: 16,
fontWeight: '700',
},
recentSection: {
marginTop: spacing.lg,
alignSelf: 'center',
width: '80%',
},
recentSectionTablet: {
maxWidth: 700,
alignSelf: 'center',
width: '100%',
},
recentItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: spacing.sm,
gap: spacing.sm,
},
recentText: {
flex: 1,
fontSize: 15,
},
list: {
paddingBottom: spacing.xl,
},
listTablet: {
maxWidth: 700,
alignSelf: 'center',
width: '100%',
},
errorContainer: {
padding: spacing.md,
marginHorizontal: spacing.lg,
borderRadius: borderRadius.lg,
},
errorContainerTablet: {
maxWidth: 700,
alignSelf: 'center',
},
errorText: {
textAlign: 'center',
fontSize: 14,
},
emptyContainer: {
padding: spacing.xl,
alignItems: 'center',
},
emptyText: {
fontSize: 16,
},
});