fix: add i18n support for parapharmacy products and notification bell
Run Tests on Branches / Detect Changes (push) Successful in 12s
Run Tests on Branches / Backend Tests (push) Has been skipped
Run Tests on Branches / Frontend Tests (push) Successful in 1m37s
Run Tests on Branches / Frontend Mobile Tests (push) Successful in 1m52s
Run Tests on Branches / Parapharmacy API Tests (push) Has been skipped
Run Tests on Branches / PIP Platform Tests (push) Has been skipped
Run Tests on Branches / Detect Changes (push) Successful in 12s
Run Tests on Branches / Backend Tests (push) Has been skipped
Run Tests on Branches / Frontend Tests (push) Successful in 1m37s
Run Tests on Branches / Frontend Mobile Tests (push) Successful in 1m52s
Run Tests on Branches / Parapharmacy API Tests (push) Has been skipped
Run Tests on Branches / PIP Platform Tests (push) Has been skipped
- Mobile: Replace hardcoded Spanish in map.tsx with t() calls - Mobile: Add notification bell to parapharmacy product detail screen - Mobile: Add map/product i18n keys to es.js and ca.js locales - Web: Add useTranslation to ProductView.jsx, replace all hardcoded strings - Web: Add 24 productView.* translation keys to es.js and ca.js locales - Fix 'Disponible en X farmacias' to respect language setting
This commit is contained in:
@@ -7,10 +7,12 @@ import { LoadingSpinner } from '../../components/LoadingSpinner';
|
|||||||
import { useThemeContext } from '../../components/ThemeProvider';
|
import { useThemeContext } from '../../components/ThemeProvider';
|
||||||
import { spacing, borderRadius } from '../../constants/theme';
|
import { spacing, borderRadius } from '../../constants/theme';
|
||||||
import { Pharmacy } from '../../types';
|
import { Pharmacy } from '../../types';
|
||||||
|
import { useTranslation } from '../../src/i18n';
|
||||||
|
|
||||||
export default function MapScreen() {
|
export default function MapScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { colors } = useThemeContext();
|
const { colors } = useThemeContext();
|
||||||
|
const { t } = useTranslation();
|
||||||
const [pharmacies, setPharmacies] = useState<Pharmacy[]>([]);
|
const [pharmacies, setPharmacies] = useState<Pharmacy[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [region, setRegion] = useState({
|
const [region, setRegion] = useState({
|
||||||
@@ -45,7 +47,7 @@ export default function MapScreen() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <LoadingSpinner message="Cargando farmacias..." />;
|
return <LoadingSpinner message={t('map.loadingPharmacies')} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -73,7 +75,7 @@ export default function MapScreen() {
|
|||||||
|
|
||||||
<View style={[styles.legend, { backgroundColor: colors.card }]}>
|
<View style={[styles.legend, { backgroundColor: colors.card }]}>
|
||||||
<Text style={[styles.legendText, { color: colors.text }]}>
|
<Text style={[styles.legendText, { color: colors.text }]}>
|
||||||
{pharmacies.length} farmacias en el mapa
|
{t('map.pharmaciesOnMap', { count: pharmacies.length })}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { View, Text, ScrollView, StyleSheet, Image, ActivityIndicator } from 'react-native';
|
import { View, Text, ScrollView, StyleSheet, Image, TouchableOpacity } from 'react-native';
|
||||||
import { useLocalSearchParams } from 'expo-router';
|
import { useLocalSearchParams } from 'expo-router';
|
||||||
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { getProduct, Product } from '../../../services/products';
|
import { getProduct, Product } from '../../../services/products';
|
||||||
|
import { subscribeToMedicine, unsubscribeFromMedicine } from '../../../services/notifications';
|
||||||
|
import { useAuth } from '../../../hooks/useAuth';
|
||||||
import { LoadingSpinner } from '../../../components/LoadingSpinner';
|
import { LoadingSpinner } from '../../../components/LoadingSpinner';
|
||||||
import { useThemeContext } from '../../../components/ThemeProvider';
|
import { useThemeContext } from '../../../components/ThemeProvider';
|
||||||
import { spacing, borderRadius } from '../../../constants/theme';
|
import { spacing, borderRadius } from '../../../constants/theme';
|
||||||
@@ -11,9 +14,12 @@ export default function ProductDetailScreen() {
|
|||||||
const { source, id } = useLocalSearchParams<{ source: string; id: string }>();
|
const { source, id } = useLocalSearchParams<{ source: string; id: string }>();
|
||||||
const { colors } = useThemeContext();
|
const { colors } = useThemeContext();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { isAuthenticated } = useAuth();
|
||||||
const [product, setProduct] = useState<Product | null>(null);
|
const [product, setProduct] = useState<Product | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
|
const [isSubscribed, setIsSubscribed] = useState(false);
|
||||||
|
const [togglingSub, setTogglingSub] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!source || !id) return;
|
if (!source || !id) return;
|
||||||
@@ -36,6 +42,25 @@ export default function ProductDetailScreen() {
|
|||||||
fetchProduct();
|
fetchProduct();
|
||||||
}, [source, id]);
|
}, [source, id]);
|
||||||
|
|
||||||
|
const handleToggleSubscription = async () => {
|
||||||
|
if (!isAuthenticated || !id) return;
|
||||||
|
setTogglingSub(true);
|
||||||
|
const productId = product?._id || product?.id || id;
|
||||||
|
try {
|
||||||
|
if (isSubscribed) {
|
||||||
|
await unsubscribeFromMedicine(productId);
|
||||||
|
setIsSubscribed(false);
|
||||||
|
} else {
|
||||||
|
await subscribeToMedicine(productId, product?.name || null);
|
||||||
|
setIsSubscribed(true);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// silently fail
|
||||||
|
} finally {
|
||||||
|
setTogglingSub(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <LoadingSpinner message={t('product.loading')} />;
|
return <LoadingSpinner message={t('product.loading')} />;
|
||||||
}
|
}
|
||||||
@@ -63,8 +88,23 @@ export default function ProductDetailScreen() {
|
|||||||
<View style={[styles.header, { backgroundColor: colors.card }]}>
|
<View style={[styles.header, { backgroundColor: colors.card }]}>
|
||||||
<View style={styles.nameRow}>
|
<View style={styles.nameRow}>
|
||||||
<Text style={[styles.name, { color: colors.text }]}>{product.name}</Text>
|
<Text style={[styles.name, { color: colors.text }]}>{product.name}</Text>
|
||||||
<View style={[styles.badge, { backgroundColor: isCima ? '#2b5bb5' : '#4caf50' }]}>
|
<View style={styles.headerRight}>
|
||||||
<Text style={styles.badgeText}>{isCima ? 'CIMA' : t('product.parapharmacy')}</Text>
|
<View style={[styles.badge, { backgroundColor: isCima ? '#2b5bb5' : '#4caf50' }]}>
|
||||||
|
<Text style={styles.badgeText}>{isCima ? 'CIMA' : t('product.parapharmacy')}</Text>
|
||||||
|
</View>
|
||||||
|
{isAuthenticated && (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.bellButton, { backgroundColor: isSubscribed ? colors.primary : colors.surfaceVariant }]}
|
||||||
|
onPress={handleToggleSubscription}
|
||||||
|
disabled={togglingSub}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name={isSubscribed ? 'notifications' : 'notifications-outline'}
|
||||||
|
size={20}
|
||||||
|
color={isSubscribed ? '#fff' : colors.textSecondary}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{product.brand ? (
|
{product.brand ? (
|
||||||
@@ -171,6 +211,18 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 22,
|
fontSize: 22,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
},
|
},
|
||||||
|
headerRight: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: spacing.sm,
|
||||||
|
},
|
||||||
|
bellButton: {
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
borderRadius: 18,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
badge: {
|
badge: {
|
||||||
paddingHorizontal: spacing.sm,
|
paddingHorizontal: spacing.sm,
|
||||||
paddingVertical: spacing.xs,
|
paddingVertical: spacing.xs,
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ const ca = {
|
|||||||
'search.parapharmacy': 'Parafarmàcia',
|
'search.parapharmacy': 'Parafarmàcia',
|
||||||
'search.error': 'Error en cercar medicaments',
|
'search.error': 'Error en cercar medicaments',
|
||||||
|
|
||||||
|
// Map
|
||||||
|
'map.loadingPharmacies': 'Carregant farmàcies...',
|
||||||
|
'map.pharmaciesOnMap': '{{count}} farmàcies al mapa',
|
||||||
|
|
||||||
// Login
|
// Login
|
||||||
'login.tab.login': 'Iniciar Sessió',
|
'login.tab.login': 'Iniciar Sessió',
|
||||||
'login.tab.register': 'Crear Compte',
|
'login.tab.register': 'Crear Compte',
|
||||||
@@ -210,6 +214,9 @@ const ca = {
|
|||||||
'product.brand': 'Marca',
|
'product.brand': 'Marca',
|
||||||
'product.source': 'Font',
|
'product.source': 'Font',
|
||||||
'product.images': 'Imatges',
|
'product.images': 'Imatges',
|
||||||
|
'product.availableInPharmacies': 'Disponible a {{count}} farmàcia/es',
|
||||||
|
'product.notifyWhenAvailable': 'Notificar-me quan hi hagi estoc',
|
||||||
|
'product.notificationsActive': 'Notificacions actives',
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
'medicineCard.noPrice': 'Sense preu',
|
'medicineCard.noPrice': 'Sense preu',
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ const es = {
|
|||||||
'search.parapharmacy': 'Parafarmacia',
|
'search.parapharmacy': 'Parafarmacia',
|
||||||
'search.error': 'Error al buscar medicamentos',
|
'search.error': 'Error al buscar medicamentos',
|
||||||
|
|
||||||
|
// Map
|
||||||
|
'map.loadingPharmacies': 'Cargando farmacias...',
|
||||||
|
'map.pharmaciesOnMap': '{{count}} farmacias en el mapa',
|
||||||
|
|
||||||
// Scanner
|
// Scanner
|
||||||
'scanner.title': 'Escanear TSI',
|
'scanner.title': 'Escanear TSI',
|
||||||
'scanner.description': 'Escanea el código de barras de tu tarjeta sanitaria para encontrar tus medicamentos',
|
'scanner.description': 'Escanea el código de barras de tu tarjeta sanitaria para encontrar tus medicamentos',
|
||||||
@@ -210,6 +214,9 @@ const es = {
|
|||||||
'product.brand': 'Marca',
|
'product.brand': 'Marca',
|
||||||
'product.source': 'Fuente',
|
'product.source': 'Fuente',
|
||||||
'product.images': 'Imágenes',
|
'product.images': 'Imágenes',
|
||||||
|
'product.availableInPharmacies': 'Disponible en {{count}} farmacia(s)',
|
||||||
|
'product.notifyWhenAvailable': 'Notificarme cuando haya stock',
|
||||||
|
'product.notificationsActive': 'Notificaciones activas',
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
'medicineCard.noPrice': 'Sin precio',
|
'medicineCard.noPrice': 'Sin precio',
|
||||||
|
|||||||
@@ -81,6 +81,32 @@ const ca = {
|
|||||||
'product.dosage': 'Dosificació:',
|
'product.dosage': 'Dosificació:',
|
||||||
'product.viewDetails': 'Veure detalls →',
|
'product.viewDetails': 'Veure detalls →',
|
||||||
|
|
||||||
|
// ProductView (detail)
|
||||||
|
'productView.loading': 'Carregant...',
|
||||||
|
'productView.back': 'Tornar',
|
||||||
|
'productView.notFound': 'Producte no trobat',
|
||||||
|
'productView.parapharmacy': 'Parafarmàcia',
|
||||||
|
'productView.activeIngredient': 'Principi actiu',
|
||||||
|
'productView.dosage': 'Dosificació',
|
||||||
|
'productView.form': 'Forma farmacèutica',
|
||||||
|
'productView.prescription': 'Prescripció',
|
||||||
|
'productView.commercialized': 'Comercialitzat',
|
||||||
|
'productView.yes': 'Sí',
|
||||||
|
'productView.no': 'No',
|
||||||
|
'productView.price': 'Preu',
|
||||||
|
'productView.previousPrice': 'Preu anterior',
|
||||||
|
'productView.category': 'Categoria',
|
||||||
|
'productView.brand': 'Marca',
|
||||||
|
'productView.source': 'Font',
|
||||||
|
'productView.loadingPharmacies': 'Carregant farmàcies...',
|
||||||
|
'productView.availableInPharmacies': 'Disponible a {{count}} farmàcia/es',
|
||||||
|
'productView.locating': '📍 Localitzant…',
|
||||||
|
'productView.sortedByDistance': '📍 Ordenat per distància · Restablir',
|
||||||
|
'productView.sortByDistance': '📍 Ordenar per distància',
|
||||||
|
'productView.usingLocation': 'Usant la vostra ubicació',
|
||||||
|
'productView.retry': 'Tornar a provar',
|
||||||
|
'productView.productNotFound': 'Producte no trobat',
|
||||||
|
|
||||||
// LoginModal
|
// LoginModal
|
||||||
'login.login': 'Iniciar sessió',
|
'login.login': 'Iniciar sessió',
|
||||||
'login.register': 'Crear compte',
|
'login.register': 'Crear compte',
|
||||||
|
|||||||
@@ -81,6 +81,32 @@ const es = {
|
|||||||
'product.dosage': 'Dosis:',
|
'product.dosage': 'Dosis:',
|
||||||
'product.viewDetails': 'Ver detalles →',
|
'product.viewDetails': 'Ver detalles →',
|
||||||
|
|
||||||
|
// ProductView (detail)
|
||||||
|
'productView.loading': 'Cargando...',
|
||||||
|
'productView.back': 'Volver',
|
||||||
|
'productView.notFound': 'Producto no encontrado',
|
||||||
|
'productView.parapharmacy': 'Parafarmacia',
|
||||||
|
'productView.activeIngredient': 'Principio activo',
|
||||||
|
'productView.dosage': 'Dosis',
|
||||||
|
'productView.form': 'Forma farmacéutica',
|
||||||
|
'productView.prescription': 'Prescripción',
|
||||||
|
'productView.commercialized': 'Comercializado',
|
||||||
|
'productView.yes': 'Sí',
|
||||||
|
'productView.no': 'No',
|
||||||
|
'productView.price': 'Precio',
|
||||||
|
'productView.previousPrice': 'Precio anterior',
|
||||||
|
'productView.category': 'Categoría',
|
||||||
|
'productView.brand': 'Marca',
|
||||||
|
'productView.source': 'Fuente',
|
||||||
|
'productView.loadingPharmacies': 'Cargando farmacias...',
|
||||||
|
'productView.availableInPharmacies': 'Disponible en {{count}} farmacia(s)',
|
||||||
|
'productView.locating': '📍 Localizando…',
|
||||||
|
'productView.sortedByDistance': '📍 Ordenado por distancia · Reset',
|
||||||
|
'productView.sortByDistance': '📍 Ordenar por distancia',
|
||||||
|
'productView.usingLocation': 'Usando tu ubicación',
|
||||||
|
'productView.retry': 'Reintentar',
|
||||||
|
'productView.productNotFound': 'Producto no encontrado',
|
||||||
|
|
||||||
// LoginModal
|
// LoginModal
|
||||||
'login.login': 'Iniciar sesión',
|
'login.login': 'Iniciar sesión',
|
||||||
'login.register': 'Crear cuenta',
|
'login.register': 'Crear cuenta',
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import React, { useState, useEffect, useMemo } from 'react';
|
|||||||
import PharmacyMap from '../components/PharmacyMap';
|
import PharmacyMap from '../components/PharmacyMap';
|
||||||
import PharmacyList from '../components/PharmacyList';
|
import PharmacyList from '../components/PharmacyList';
|
||||||
import { haversineKm, getUserPosition, hasCachedPosition } from '../utils/geo';
|
import { haversineKm, getUserPosition, hasCachedPosition } from '../utils/geo';
|
||||||
|
import { useTranslation } from '../i18n';
|
||||||
import './ProductView.css';
|
import './ProductView.css';
|
||||||
|
|
||||||
export default function ProductView({ source, id, onBack }) {
|
export default function ProductView({ source, id, onBack }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [product, setProduct] = useState(null);
|
const [product, setProduct] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
@@ -43,11 +45,11 @@ export default function ProductView({ source, id, onBack }) {
|
|||||||
setPositionSource('browser');
|
setPositionSource('browser');
|
||||||
setSortByDistance(true);
|
setSortByDistance(true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
let msg = 'No se pudo obtener tu ubicación';
|
let msg = t('search.locationError');
|
||||||
if (err && typeof err.code === 'number') {
|
if (err && typeof err.code === 'number') {
|
||||||
if (err.code === 1) msg = 'Permiso de ubicación denegado.';
|
if (err.code === 1) msg = t('search.locationDenied');
|
||||||
else if (err.code === 2) msg = 'Ubicación no disponible.';
|
else if (err.code === 2) msg = t('search.locationUnavailable');
|
||||||
else if (err.code === 3) msg = 'La ubicación tardó demasiado.';
|
else if (err.code === 3) msg = t('search.locationTimeout');
|
||||||
}
|
}
|
||||||
setLocationError(msg);
|
setLocationError(msg);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -72,7 +74,7 @@ export default function ProductView({ source, id, onBack }) {
|
|||||||
|
|
||||||
const response = await fetch(apiUrl);
|
const response = await fetch(apiUrl);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Producto no encontrado');
|
throw new Error(t('productView.productNotFound'));
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setProduct(data);
|
setProduct(data);
|
||||||
@@ -102,7 +104,7 @@ export default function ProductView({ source, id, onBack }) {
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="product-view">
|
<div className="product-view">
|
||||||
<div className="product-loading">Cargando...</div>
|
<div className="product-loading">{t('productView.loading')}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -114,7 +116,7 @@ export default function ProductView({ source, id, onBack }) {
|
|||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
<path d="M19 12H5M12 19l-7-7 7-7" />
|
<path d="M19 12H5M12 19l-7-7 7-7" />
|
||||||
</svg>
|
</svg>
|
||||||
Volver
|
{t('productView.back')}
|
||||||
</button>
|
</button>
|
||||||
<div className="product-error">{error}</div>
|
<div className="product-error">{error}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -126,7 +128,7 @@ export default function ProductView({ source, id, onBack }) {
|
|||||||
const isCima = product.source === 'cima';
|
const isCima = product.source === 'cima';
|
||||||
const isParapharmacy = product.source !== 'cima';
|
const isParapharmacy = product.source !== 'cima';
|
||||||
const sourceColor = isCima ? '#2563eb' : '#16a34a';
|
const sourceColor = isCima ? '#2563eb' : '#16a34a';
|
||||||
const sourceLabel = isCima ? 'CIMA' : 'Parafarmacia';
|
const sourceLabel = isCima ? 'CIMA' : t('productView.parapharmacy');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="product-view">
|
<div className="product-view">
|
||||||
@@ -134,7 +136,7 @@ export default function ProductView({ source, id, onBack }) {
|
|||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
<path d="M19 12H5M12 19l-7-7 7-7" />
|
<path d="M19 12H5M12 19l-7-7 7-7" />
|
||||||
</svg>
|
</svg>
|
||||||
Volver
|
{t('productView.back')}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="product-header">
|
<div className="product-header">
|
||||||
@@ -153,37 +155,37 @@ export default function ProductView({ source, id, onBack }) {
|
|||||||
{isCima ? (
|
{isCima ? (
|
||||||
<>
|
<>
|
||||||
{product.active_ingredient && (
|
{product.active_ingredient && (
|
||||||
<DetailRow label="Principio activo" value={product.active_ingredient} />
|
<DetailRow label={t('productView.activeIngredient')} value={product.active_ingredient} />
|
||||||
)}
|
)}
|
||||||
{product.dosage && (
|
{product.dosage && (
|
||||||
<DetailRow label="Dosis" value={product.dosage} />
|
<DetailRow label={t('productView.dosage')} value={product.dosage} />
|
||||||
)}
|
)}
|
||||||
{product.form && (
|
{product.form && (
|
||||||
<DetailRow label="Forma farmacéutica" value={product.form} />
|
<DetailRow label={t('productView.form')} value={product.form} />
|
||||||
)}
|
)}
|
||||||
{product.prescription && (
|
{product.prescription && (
|
||||||
<DetailRow label="Prescripción" value={product.prescription} />
|
<DetailRow label={t('productView.prescription')} value={product.prescription} />
|
||||||
)}
|
)}
|
||||||
{product.commercialized !== undefined && (
|
{product.commercialized !== undefined && (
|
||||||
<DetailRow label="Comercializado" value={product.commercialized ? 'Sí' : 'No'} />
|
<DetailRow label={t('productView.commercialized')} value={product.commercialized ? t('productView.yes') : t('productView.no')} />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{product.price && (
|
{product.price && (
|
||||||
<DetailRow label="Precio" value={`${product.price} €`} />
|
<DetailRow label={t('productView.price')} value={`${product.price} €`} />
|
||||||
)}
|
)}
|
||||||
{product.original_price && product.original_price > product.price && (
|
{product.original_price && product.original_price > product.price && (
|
||||||
<DetailRow label="Precio anterior" value={`${product.original_price} €`} />
|
<DetailRow label={t('productView.previousPrice')} value={`${product.original_price} €`} />
|
||||||
)}
|
)}
|
||||||
{product.category && (
|
{product.category && (
|
||||||
<DetailRow label="Categoría" value={product.category} />
|
<DetailRow label={t('productView.category')} value={product.category} />
|
||||||
)}
|
)}
|
||||||
{product.brand && (
|
{product.brand && (
|
||||||
<DetailRow label="Marca" value={product.brand} />
|
<DetailRow label={t('productView.brand')} value={product.brand} />
|
||||||
)}
|
)}
|
||||||
{product.source_url && (
|
{product.source_url && (
|
||||||
<DetailRow label="Fuente" value={product.source} />
|
<DetailRow label={t('productView.source')} value={product.source} />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -191,11 +193,11 @@ export default function ProductView({ source, id, onBack }) {
|
|||||||
|
|
||||||
<div className="product-pharmacies">
|
<div className="product-pharmacies">
|
||||||
{loadingPharmacies ? (
|
{loadingPharmacies ? (
|
||||||
<div className="pharmacies-loading">Cargando farmacias...</div>
|
<div className="pharmacies-loading">{t('productView.loadingPharmacies')}</div>
|
||||||
) : pharmacies.length > 0 ? (
|
) : pharmacies.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<h3 className="pharmacies-title">
|
<h3 className="pharmacies-title">
|
||||||
Disponible en {pharmacies.length} {pharmacies.length === 1 ? 'farmacia' : 'farmacias'}
|
{t('productView.availableInPharmacies', { count: pharmacies.length })}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div className="pharmacy-controls">
|
<div className="pharmacy-controls">
|
||||||
@@ -205,19 +207,19 @@ export default function ProductView({ source, id, onBack }) {
|
|||||||
disabled={locating}
|
disabled={locating}
|
||||||
>
|
>
|
||||||
{locating
|
{locating
|
||||||
? '📍 Localizando…'
|
? t('productView.locating')
|
||||||
: sortByDistance
|
: sortByDistance
|
||||||
? '📍 Ordenado por distancia · Reset'
|
? t('productView.sortedByDistance')
|
||||||
: '📍 Ordenar por distancia'}
|
: t('productView.sortByDistance')}
|
||||||
</button>
|
</button>
|
||||||
{sortByDistance && positionSource && (
|
{sortByDistance && positionSource && (
|
||||||
<span className="location-source">Usando tu ubicación</span>
|
<span className="location-source">{t('productView.usingLocation')}</span>
|
||||||
)}
|
)}
|
||||||
{locationError && (
|
{locationError && (
|
||||||
<span className="location-error">
|
<span className="location-error">
|
||||||
{locationError}
|
{locationError}
|
||||||
<button className="retry-location-btn" onClick={handleSortByDistance}>
|
<button className="retry-location-btn" onClick={handleSortByDistance}>
|
||||||
Reintentar
|
{t('productView.retry')}
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user