From 39331e2f6efd51bee1233b405cc2e3ab36b100b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoni=20Nu=C3=B1ez=20Romeu?= Date: Mon, 6 Jul 2026 11:08:40 +0200 Subject: [PATCH] feat: add UI components (SearchBar, MedicineCard, StockBadge, LoadingSpinner) --- frontend-mobile/components/LoadingSpinner.tsx | 30 +++++ frontend-mobile/components/MedicineCard.tsx | 110 ++++++++++++++++++ frontend-mobile/components/SearchBar.tsx | 81 +++++++++++++ frontend-mobile/components/StockBadge.tsx | 48 ++++++++ 4 files changed, 269 insertions(+) create mode 100644 frontend-mobile/components/LoadingSpinner.tsx create mode 100644 frontend-mobile/components/MedicineCard.tsx create mode 100644 frontend-mobile/components/SearchBar.tsx create mode 100644 frontend-mobile/components/StockBadge.tsx diff --git a/frontend-mobile/components/LoadingSpinner.tsx b/frontend-mobile/components/LoadingSpinner.tsx new file mode 100644 index 0000000..ad24212 --- /dev/null +++ b/frontend-mobile/components/LoadingSpinner.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { View, ActivityIndicator, Text, StyleSheet } from 'react-native'; +import { colors, spacing } from '../constants/theme'; + +interface LoadingSpinnerProps { + message?: string; +} + +export function LoadingSpinner({ message = 'Cargando...' }: LoadingSpinnerProps) { + return ( + + + {message} + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: spacing.xl, + }, + message: { + marginTop: spacing.md, + fontSize: 16, + color: colors.textSecondary, + }, +}); diff --git a/frontend-mobile/components/MedicineCard.tsx b/frontend-mobile/components/MedicineCard.tsx new file mode 100644 index 0000000..5a5f91d --- /dev/null +++ b/frontend-mobile/components/MedicineCard.tsx @@ -0,0 +1,110 @@ +import React from 'react'; +import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; +import { useRouter } from 'expo-router'; +import { Ionicons } from '@expo/vector-icons'; +import { colors, spacing, borderRadius } from '../constants/theme'; +import { StockBadge } from './StockBadge'; +import { Medicine } from '../types'; + +interface MedicineCardProps { + medicine: Medicine; +} + +export function MedicineCard({ medicine }: MedicineCardProps) { + const router = useRouter(); + + const handlePress = () => { + router.push(`/medicine/${medicine.nregistro}`); + }; + + return ( + + + + {medicine.nombre} + + + + + + {medicine.principioActivo} + + + + + + + {medicine.precio ? `${medicine.precio.toFixed(2)} €` : 'Sin precio'} + + + + + + + {medicine.laboratorio} + + + + + ); +} + +const styles = StyleSheet.create({ + card: { + backgroundColor: colors.card, + borderRadius: borderRadius.md, + padding: spacing.md, + marginHorizontal: spacing.md, + marginVertical: spacing.xs, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 2, + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start', + marginBottom: spacing.xs, + }, + name: { + flex: 1, + fontSize: 16, + fontWeight: '600', + color: colors.text, + marginRight: spacing.sm, + }, + principioActivo: { + fontSize: 14, + color: colors.textSecondary, + marginBottom: spacing.sm, + }, + footer: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + priceContainer: { + flexDirection: 'row', + alignItems: 'center', + }, + price: { + fontSize: 14, + fontWeight: '600', + color: colors.text, + marginLeft: spacing.xs, + }, + labContainer: { + flexDirection: 'row', + alignItems: 'center', + flex: 1, + justifyContent: 'flex-end', + }, + laboratorio: { + fontSize: 12, + color: colors.textSecondary, + marginLeft: spacing.xs, + maxWidth: 120, + }, +}); diff --git a/frontend-mobile/components/SearchBar.tsx b/frontend-mobile/components/SearchBar.tsx new file mode 100644 index 0000000..d2abe4e --- /dev/null +++ b/frontend-mobile/components/SearchBar.tsx @@ -0,0 +1,81 @@ +import React, { useState } from 'react'; +import { View, TextInput, StyleSheet, TouchableOpacity } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { colors, spacing, borderRadius } from '../constants/theme'; + +interface SearchBarProps { + placeholder?: string; + onSearch: (query: string) => void; + value?: string; + onChangeText?: (text: string) => void; +} + +export function SearchBar({ + placeholder = 'Buscar medicamentos...', + onSearch, + value, + onChangeText +}: SearchBarProps) { + const [localValue, setLocalValue] = useState(value || ''); + + const handleChange = (text: string) => { + setLocalValue(text); + onChangeText?.(text); + }; + + const handleSubmit = () => { + onSearch(localValue); + }; + + const handleClear = () => { + setLocalValue(''); + onChangeText?.(''); + onSearch(''); + }; + + return ( + + + + {localValue.length > 0 && ( + + + + )} + + ); +} + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: colors.card, + borderRadius: borderRadius.md, + paddingHorizontal: spacing.md, + paddingVertical: spacing.sm, + marginHorizontal: spacing.md, + marginVertical: spacing.sm, + }, + icon: { + marginRight: spacing.sm, + }, + input: { + flex: 1, + fontSize: 16, + color: colors.text, + paddingVertical: spacing.xs, + }, + clearButton: { + marginLeft: spacing.sm, + }, +}); diff --git a/frontend-mobile/components/StockBadge.tsx b/frontend-mobile/components/StockBadge.tsx new file mode 100644 index 0000000..491eb35 --- /dev/null +++ b/frontend-mobile/components/StockBadge.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { View, Text, StyleSheet } from 'react-native'; +import { colors, borderRadius, spacing } from '../constants/theme'; + +interface StockBadgeProps { + stock: number; +} + +export function StockBadge({ stock }: StockBadgeProps) { + const getBadgeStyle = () => { + if (stock === 0) return styles.danger; + if (stock < 5) return styles.warning; + return styles.success; + }; + + const getText = () => { + if (stock === 0) return 'Sin stock'; + if (stock < 5) return `Bajo (${stock})`; + return `Disponible (${stock})`; + }; + + return ( + + {getText()} + + ); +} + +const styles = StyleSheet.create({ + badge: { + paddingHorizontal: spacing.sm, + paddingVertical: spacing.xs, + borderRadius: borderRadius.sm, + }, + success: { + backgroundColor: '#D4EDDA', + }, + warning: { + backgroundColor: '#FFF3CD', + }, + danger: { + backgroundColor: '#F8D7DA', + }, + text: { + fontSize: 12, + fontWeight: '600', + }, +});