new_react_frontend #9
@@ -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 (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<ActivityIndicator size="large" color={colors.primary} />
|
||||||
|
<Text style={styles.message}>{message}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: spacing.xl,
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
marginTop: spacing.md,
|
||||||
|
fontSize: 16,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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 (
|
||||||
|
<TouchableOpacity style={styles.card} onPress={handlePress} activeOpacity={0.7}>
|
||||||
|
<View style={styles.header}>
|
||||||
|
<Text style={styles.name} numberOfLines={2}>
|
||||||
|
{medicine.nombre}
|
||||||
|
</Text>
|
||||||
|
<StockBadge stock={medicine.stock} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={styles.principioActivo} numberOfLines={1}>
|
||||||
|
{medicine.principioActivo}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<View style={styles.footer}>
|
||||||
|
<View style={styles.priceContainer}>
|
||||||
|
<Ionicons name="pricetag" size={14} color={colors.textSecondary} />
|
||||||
|
<Text style={styles.price}>
|
||||||
|
{medicine.precio ? `${medicine.precio.toFixed(2)} €` : 'Sin precio'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.labContainer}>
|
||||||
|
<Ionicons name="business" size={14} color={colors.textSecondary} />
|
||||||
|
<Text style={styles.laboratorio} numberOfLines={1}>
|
||||||
|
{medicine.laboratorio}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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 (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Ionicons name="search" size={20} color={colors.textSecondary} style={styles.icon} />
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
placeholder={placeholder}
|
||||||
|
placeholderTextColor={colors.textSecondary}
|
||||||
|
value={localValue}
|
||||||
|
onChangeText={handleChange}
|
||||||
|
onSubmitEditing={handleSubmit}
|
||||||
|
returnKeyType="search"
|
||||||
|
autoCorrect={false}
|
||||||
|
/>
|
||||||
|
{localValue.length > 0 && (
|
||||||
|
<TouchableOpacity onPress={handleClear} style={styles.clearButton}>
|
||||||
|
<Ionicons name="close-circle" size={20} color={colors.textSecondary} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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 (
|
||||||
|
<View style={[styles.badge, getBadgeStyle()]}>
|
||||||
|
<Text style={styles.text}>{getText()}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user