Files

369 lines
12 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, useRouter } 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 { searchProducts, Product } from '../../services/products';
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 [products, setProducts] = useState<Product[]>([]);
const [searchMode, setSearchMode] = useState<'all' | 'medicines' | 'products'>('all');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const debouncedQuery = useDebounce(query, config.SEARCH_DEBOUNCE_MS);
const navigation = useNavigation();
const router = useRouter();
useEffect(() => {
const parent = navigation.getParent();
if (!parent) return;
const unsubscribe = parent.addListener('tabPress', () => {
setQuery('');
setResults([]);
setProducts([]);
setSearchMode('all');
setIsLoading(false);
setError(null);
});
return unsubscribe;
}, [navigation]);
useEffect(() => {
if (debouncedQuery.length < 2) {
setResults([]);
setProducts([]);
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);
}
};
const fetchProducts = async () => {
try {
const productResults = await searchProducts(debouncedQuery);
setProducts(productResults);
} catch (err) {
console.error('Product search error:', err);
}
};
fetchResults();
fetchProducts();
}, [debouncedQuery]);
const handleSearch = (searchQuery: string) => {
setQuery(searchQuery);
if (searchQuery.trim()) addSearch(searchQuery);
};
const showSuggestions = !query && !isLoading && results.length === 0 && products.length === 0;
return (
<View style={[styles.container, { backgroundColor: colors.background }]}>
<SearchBar
onSearch={handleSearch}
value={query}
onChangeText={setQuery}
/>
{query.length >= 2 && (
<View style={styles.filterContainer}>
<TouchableOpacity style={[styles.filterTab, searchMode === 'all' && styles.filterTabActive]} onPress={() => setSearchMode('all')}>
<Text style={[styles.filterText, searchMode === 'all' && styles.filterTextActive]}>Todos</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.filterTab, searchMode === 'medicines' && styles.filterTabActive]} onPress={() => setSearchMode('medicines')}>
<Text style={[styles.filterText, searchMode === 'medicines' && styles.filterTextActive]}>Medicamentos</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.filterTab, searchMode === 'products' && styles.filterTabActive]} onPress={() => setSearchMode('products')}>
<Text style={[styles.filterText, searchMode === 'products' && styles.filterTextActive]}>Parafarmacia</Text>
</TouchableOpacity>
</View>
)}
{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..." />}
{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 && products.length === 0 && query.length >= 2 && (
<View style={styles.emptyContainer}>
<Text style={[styles.emptyText, { color: colors.textSecondary }]}>No se encontraron resultados</Text>
</View>
)}
<FlatList
data={(searchMode === 'medicines' || searchMode === 'all') ? results : []}
keyExtractor={(item) => item.nregistro}
renderItem={({ item }) => <MedicineCard medicine={item} />}
ListHeaderComponent={
<>
{(searchMode === 'all' || searchMode === 'products') && products.length > 0 && (
<View style={styles.section}>
<Text style={[styles.sectionTitle, { color: colors.text }]}>Parafarmacia y Bebé</Text>
{products.map((product) => (
<TouchableOpacity
key={`${product.source}-${product.id}`}
style={[styles.productCard, { backgroundColor: colors.card, borderColor: colors.border }]}
onPress={() => router.push(`/product/${product.source}/${product.id}`)}
activeOpacity={0.7}
>
<View style={styles.productInfo}>
<View style={styles.badges}>
<View style={[styles.sourceBadge, { backgroundColor: product.source === 'cima' ? '#2563eb' : '#16a34a' }]}>
<Text style={styles.badgeText}>{product.source === 'cima' ? 'CIMA' : 'OFF'}</Text>
</View>
</View>
<Text style={[styles.productName, { color: colors.text }]} numberOfLines={1}>{product.name}</Text>
<Text style={[styles.productBrand, { color: colors.textSecondary }]} numberOfLines={1}>{product.brand}</Text>
</View>
</TouchableOpacity>
))}
</View>
)}
</>
}
contentContainerStyle={[styles.list, isTablet && styles.listTablet]}
showsVerticalScrollIndicator={false}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
filterContainer: {
flexDirection: 'row',
justifyContent: 'center',
gap: spacing.sm,
paddingVertical: spacing.sm,
paddingHorizontal: spacing.md,
},
filterTab: {
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
borderRadius: borderRadius.full,
backgroundColor: 'rgba(0,0,0,0.05)',
},
filterTabActive: {
backgroundColor: '#7fbf8f',
},
filterText: {
fontSize: 14,
fontWeight: '600',
color: '#41493e',
},
filterTextActive: {
color: '#ffffff',
},
section: {
paddingHorizontal: spacing.lg,
marginTop: spacing.md,
},
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,
},
productCard: {
flexDirection: 'row',
alignItems: 'center',
borderRadius: borderRadius.lg,
borderWidth: 1,
padding: spacing.md,
marginBottom: spacing.sm,
},
productInfo: {
flex: 1,
gap: 4,
},
badges: {
flexDirection: 'row',
gap: spacing.sm,
marginBottom: 2,
},
sourceBadge: {
paddingHorizontal: spacing.sm,
paddingVertical: 2,
borderRadius: borderRadius.sm,
},
badgeText: {
color: '#ffffff',
fontSize: 11,
fontWeight: '700',
},
productName: {
fontSize: 15,
fontWeight: '600',
},
productBrand: {
fontSize: 13,
},
});