feat(mobile): add product detail screen for CIMA and OFF products

This commit is contained in:
Antoni Nuñez Romeu
2026-07-13 15:33:34 +02:00
parent bb2dbbab3a
commit 7b1636a96e
@@ -0,0 +1,249 @@
import React, { useEffect, useState } from 'react';
import { View, Text, ScrollView, StyleSheet, Image, ActivityIndicator } from 'react-native';
import { useLocalSearchParams } from 'expo-router';
import { getProduct, Product } from '../../../services/products';
import { LoadingSpinner } from '../../../components/LoadingSpinner';
import { useThemeContext } from '../../../components/ThemeProvider';
import { spacing, borderRadius } from '../../../constants/theme';
export default function ProductDetailScreen() {
const { source, id } = useLocalSearchParams<{ source: string; id: string }>();
const { colors } = useThemeContext();
const [product, setProduct] = useState<Product | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(false);
useEffect(() => {
if (!source || !id) return;
const fetchProduct = async () => {
try {
const data = await getProduct(source, id);
if (data) {
setProduct(data);
} else {
setError(true);
}
} catch {
setError(true);
} finally {
setIsLoading(false);
}
};
fetchProduct();
}, [source, id]);
if (isLoading) {
return <LoadingSpinner message="Cargando producto..." />;
}
if (error || !product) {
return (
<View style={[styles.errorContainer, { backgroundColor: colors.background }]}>
<Text style={[styles.errorText, { color: colors.textSecondary }]}>Producto no encontrado</Text>
</View>
);
}
const isCima = product.source === 'cima';
return (
<ScrollView style={[styles.container, { backgroundColor: colors.background }]}>
{product.image_url ? (
<Image source={{ uri: product.image_url }} style={styles.image} resizeMode="contain" />
) : (
<View style={[styles.imagePlaceholder, { backgroundColor: colors.surfaceLow }]}>
<Text style={[styles.placeholderText, { color: colors.textSecondary }]}>Sin imagen</Text>
</View>
)}
<View style={[styles.header, { backgroundColor: colors.card }]}>
<View style={styles.nameRow}>
<Text style={[styles.name, { color: colors.text }]}>{product.name}</Text>
<View style={[styles.badge, { backgroundColor: isCima ? '#2b5bb5' : '#4caf50' }]}>
<Text style={styles.badgeText}>{isCima ? 'CIMA' : 'OFF'}</Text>
</View>
</View>
{product.brand ? (
<Text style={[styles.brand, { color: colors.textSecondary }]}>{product.brand}</Text>
) : null}
{product.category ? (
<Text style={[styles.category, { color: colors.textSecondary }]}>{product.category}</Text>
) : null}
</View>
{isCima ? (
<View style={[styles.infoSection, { backgroundColor: colors.card }]}>
<Text style={[styles.sectionTitle, { color: colors.text }]}>Detalles CIMA</Text>
{product.active_ingredient && (
<InfoRow label="Principio activo" value={product.active_ingredient} colors={colors} />
)}
{product.dosage && (
<InfoRow label="Dosificación" value={product.dosage} colors={colors} />
)}
{product.form && (
<InfoRow label="Forma farmacéutica" value={product.form} colors={colors} />
)}
{product.prescription && (
<InfoRow label="Tipo de dispensación" value={product.prescription} colors={colors} />
)}
<InfoRow label="Comercializado" value={product.commercialized ? 'Sí' : 'No'} colors={colors} />
</View>
) : (
<View style={[styles.infoSection, { backgroundColor: colors.card }]}>
<Text style={[styles.sectionTitle, { color: colors.text }]}>Información nutricional</Text>
{product.nutriscore && (
<InfoRow label="Nutri-Score" value={product.nutriscore.toUpperCase()} colors={colors} />
)}
{product.nova_group != null && (
<InfoRow label="Grupo NOVA" value={String(product.nova_group)} colors={colors} />
)}
{product.eco_score && (
<InfoRow label="Eco-Score" value={product.eco_score.toUpperCase()} colors={colors} />
)}
{product.ingredients && (
<View style={[styles.infoRow, { borderBottomColor: colors.border }]}>
<Text style={[styles.infoLabel, { color: colors.textSecondary }]}>Ingredientes</Text>
<Text style={[styles.infoValueMultiline, { color: colors.text }]}>{product.ingredients}</Text>
</View>
)}
</View>
)}
{isCima && product.photos && product.photos.length > 0 && (
<View style={[styles.infoSection, { backgroundColor: colors.card }]}>
<Text style={[styles.sectionTitle, { color: colors.text }]}>Imágenes</Text>
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.photosScroll}>
{product.photos.map((photo, i) => (
<View key={i} style={styles.photoItem}>
<Image source={{ uri: photo.url }} style={styles.photoImage} resizeMode="contain" />
<Text style={[styles.photoLabel, { color: colors.textSecondary }]}>{photo.tipo}</Text>
</View>
))}
</ScrollView>
</View>
)}
</ScrollView>
);
}
function InfoRow({ label, value, colors }: { label: string; value: string; colors: any }) {
return (
<View style={[styles.infoRow, { borderBottomColor: colors.border }]}>
<Text style={[styles.infoLabel, { color: colors.textSecondary }]}>{label}</Text>
<Text style={[styles.infoValue, { color: colors.text }]}>{value}</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
image: {
width: '100%',
height: 260,
backgroundColor: '#f5f5f5',
},
imagePlaceholder: {
width: '100%',
height: 160,
alignItems: 'center',
justifyContent: 'center',
},
placeholderText: {
fontSize: 14,
},
header: {
padding: spacing.lg,
},
nameRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
gap: spacing.sm,
},
name: {
flex: 1,
fontSize: 22,
fontWeight: 'bold',
},
badge: {
paddingHorizontal: spacing.sm,
paddingVertical: spacing.xs,
borderRadius: borderRadius.full,
overflow: 'hidden',
},
badgeText: {
color: '#fff',
fontSize: 12,
fontWeight: '700',
},
brand: {
fontSize: 15,
marginTop: spacing.xs,
},
category: {
fontSize: 13,
marginTop: spacing.xs,
},
infoSection: {
marginTop: spacing.sm,
padding: spacing.lg,
},
sectionTitle: {
fontSize: 16,
fontWeight: '600',
marginBottom: spacing.sm,
},
infoRow: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingVertical: spacing.sm,
borderBottomWidth: 1,
},
infoLabel: {
fontSize: 13,
flexShrink: 0,
marginRight: spacing.sm,
},
infoValue: {
fontSize: 13,
fontWeight: '500',
flex: 1,
textAlign: 'right',
},
infoValueMultiline: {
fontSize: 13,
fontWeight: '500',
flex: 1,
textAlign: 'right',
},
photosScroll: {
marginTop: spacing.xs,
},
photoItem: {
marginRight: spacing.md,
alignItems: 'center',
width: 120,
},
photoImage: {
width: 120,
height: 120,
borderRadius: borderRadius.md,
backgroundColor: '#f5f5f5',
},
photoLabel: {
fontSize: 11,
marginTop: spacing.xs,
},
errorContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
errorText: {
fontSize: 16,
},
});