Mobile App design
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
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,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user