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',
+ },
+});