diff --git a/apps/frontend-mobile/app/(tabs)/map.tsx b/apps/frontend-mobile/app/(tabs)/map.tsx index a9d783d..6e0d523 100644 --- a/apps/frontend-mobile/app/(tabs)/map.tsx +++ b/apps/frontend-mobile/app/(tabs)/map.tsx @@ -7,10 +7,12 @@ import { LoadingSpinner } from '../../components/LoadingSpinner'; import { useThemeContext } from '../../components/ThemeProvider'; import { spacing, borderRadius } from '../../constants/theme'; import { Pharmacy } from '../../types'; +import { useTranslation } from '../../src/i18n'; export default function MapScreen() { const router = useRouter(); const { colors } = useThemeContext(); + const { t } = useTranslation(); const [pharmacies, setPharmacies] = useState([]); const [isLoading, setIsLoading] = useState(true); const [region, setRegion] = useState({ @@ -45,7 +47,7 @@ export default function MapScreen() { }, []); if (isLoading) { - return ; + return ; } return ( @@ -73,7 +75,7 @@ export default function MapScreen() { - {pharmacies.length} farmacias en el mapa + {t('map.pharmaciesOnMap', { count: pharmacies.length })} diff --git a/apps/frontend-mobile/app/product/[source]/[id].tsx b/apps/frontend-mobile/app/product/[source]/[id].tsx index 21170ac..cedacc9 100644 --- a/apps/frontend-mobile/app/product/[source]/[id].tsx +++ b/apps/frontend-mobile/app/product/[source]/[id].tsx @@ -1,7 +1,10 @@ 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 { Ionicons } from '@expo/vector-icons'; import { getProduct, Product } from '../../../services/products'; +import { subscribeToMedicine, unsubscribeFromMedicine } from '../../../services/notifications'; +import { useAuth } from '../../../hooks/useAuth'; import { LoadingSpinner } from '../../../components/LoadingSpinner'; import { useThemeContext } from '../../../components/ThemeProvider'; import { spacing, borderRadius } from '../../../constants/theme'; @@ -11,9 +14,12 @@ export default function ProductDetailScreen() { const { source, id } = useLocalSearchParams<{ source: string; id: string }>(); const { colors } = useThemeContext(); const { t } = useTranslation(); + const { isAuthenticated } = useAuth(); const [product, setProduct] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(false); + const [isSubscribed, setIsSubscribed] = useState(false); + const [togglingSub, setTogglingSub] = useState(false); useEffect(() => { if (!source || !id) return; @@ -36,6 +42,25 @@ export default function ProductDetailScreen() { fetchProduct(); }, [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) { return ; } @@ -63,8 +88,23 @@ export default function ProductDetailScreen() { {product.name} - - {isCima ? 'CIMA' : t('product.parapharmacy')} + + + {isCima ? 'CIMA' : t('product.parapharmacy')} + + {isAuthenticated && ( + + + + )} {product.brand ? ( @@ -171,6 +211,18 @@ const styles = StyleSheet.create({ fontSize: 22, fontWeight: 'bold', }, + headerRight: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.sm, + }, + bellButton: { + width: 36, + height: 36, + borderRadius: 18, + alignItems: 'center', + justifyContent: 'center', + }, badge: { paddingHorizontal: spacing.sm, paddingVertical: spacing.xs, diff --git a/apps/frontend-mobile/src/i18n/locales/ca.js b/apps/frontend-mobile/src/i18n/locales/ca.js index 8d8c690..72ba31f 100644 --- a/apps/frontend-mobile/src/i18n/locales/ca.js +++ b/apps/frontend-mobile/src/i18n/locales/ca.js @@ -28,6 +28,10 @@ const ca = { 'search.parapharmacy': 'Parafarmàcia', 'search.error': 'Error en cercar medicaments', + // Map + 'map.loadingPharmacies': 'Carregant farmàcies...', + 'map.pharmaciesOnMap': '{{count}} farmàcies al mapa', + // Login 'login.tab.login': 'Iniciar Sessió', 'login.tab.register': 'Crear Compte', @@ -210,6 +214,9 @@ const ca = { 'product.brand': 'Marca', 'product.source': 'Font', 'product.images': 'Imatges', + 'product.availableInPharmacies': 'Disponible a {{count}} farmàcia/es', + 'product.notifyWhenAvailable': 'Notificar-me quan hi hagi estoc', + 'product.notificationsActive': 'Notificacions actives', // Components 'medicineCard.noPrice': 'Sense preu', diff --git a/apps/frontend-mobile/src/i18n/locales/es.js b/apps/frontend-mobile/src/i18n/locales/es.js index 7157343..36fc34f 100644 --- a/apps/frontend-mobile/src/i18n/locales/es.js +++ b/apps/frontend-mobile/src/i18n/locales/es.js @@ -56,6 +56,10 @@ const es = { 'search.parapharmacy': 'Parafarmacia', 'search.error': 'Error al buscar medicamentos', + // Map + 'map.loadingPharmacies': 'Cargando farmacias...', + 'map.pharmaciesOnMap': '{{count}} farmacias en el mapa', + // Scanner 'scanner.title': 'Escanear TSI', '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.source': 'Fuente', 'product.images': 'Imágenes', + 'product.availableInPharmacies': 'Disponible en {{count}} farmacia(s)', + 'product.notifyWhenAvailable': 'Notificarme cuando haya stock', + 'product.notificationsActive': 'Notificaciones activas', // Components 'medicineCard.noPrice': 'Sin precio', diff --git a/apps/frontend/src/i18n/locales/ca.js b/apps/frontend/src/i18n/locales/ca.js index 9e01b22..d092d6d 100644 --- a/apps/frontend/src/i18n/locales/ca.js +++ b/apps/frontend/src/i18n/locales/ca.js @@ -81,6 +81,32 @@ const ca = { 'product.dosage': 'Dosificació:', '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 'login.login': 'Iniciar sessió', 'login.register': 'Crear compte', diff --git a/apps/frontend/src/i18n/locales/es.js b/apps/frontend/src/i18n/locales/es.js index 0015425..e683a57 100644 --- a/apps/frontend/src/i18n/locales/es.js +++ b/apps/frontend/src/i18n/locales/es.js @@ -81,6 +81,32 @@ const es = { 'product.dosage': 'Dosis:', '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 'login.login': 'Iniciar sesión', 'login.register': 'Crear cuenta', diff --git a/apps/frontend/src/views/ProductView.jsx b/apps/frontend/src/views/ProductView.jsx index c6493ad..cc4553b 100644 --- a/apps/frontend/src/views/ProductView.jsx +++ b/apps/frontend/src/views/ProductView.jsx @@ -2,9 +2,11 @@ import React, { useState, useEffect, useMemo } from 'react'; import PharmacyMap from '../components/PharmacyMap'; import PharmacyList from '../components/PharmacyList'; import { haversineKm, getUserPosition, hasCachedPosition } from '../utils/geo'; +import { useTranslation } from '../i18n'; import './ProductView.css'; export default function ProductView({ source, id, onBack }) { + const { t } = useTranslation(); const [product, setProduct] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -43,11 +45,11 @@ export default function ProductView({ source, id, onBack }) { setPositionSource('browser'); setSortByDistance(true); } catch (err) { - let msg = 'No se pudo obtener tu ubicación'; + let msg = t('search.locationError'); if (err && typeof err.code === 'number') { - if (err.code === 1) msg = 'Permiso de ubicación denegado.'; - else if (err.code === 2) msg = 'Ubicación no disponible.'; - else if (err.code === 3) msg = 'La ubicación tardó demasiado.'; + if (err.code === 1) msg = t('search.locationDenied'); + else if (err.code === 2) msg = t('search.locationUnavailable'); + else if (err.code === 3) msg = t('search.locationTimeout'); } setLocationError(msg); } finally { @@ -72,7 +74,7 @@ export default function ProductView({ source, id, onBack }) { const response = await fetch(apiUrl); if (!response.ok) { - throw new Error('Producto no encontrado'); + throw new Error(t('productView.productNotFound')); } const data = await response.json(); setProduct(data); @@ -102,7 +104,7 @@ export default function ProductView({ source, id, onBack }) { if (loading) { return (
-
Cargando...
+
{t('productView.loading')}
); } @@ -114,7 +116,7 @@ export default function ProductView({ source, id, onBack }) { - Volver + {t('productView.back')}
{error}
@@ -126,7 +128,7 @@ export default function ProductView({ source, id, onBack }) { const isCima = product.source === 'cima'; const isParapharmacy = product.source !== 'cima'; const sourceColor = isCima ? '#2563eb' : '#16a34a'; - const sourceLabel = isCima ? 'CIMA' : 'Parafarmacia'; + const sourceLabel = isCima ? 'CIMA' : t('productView.parapharmacy'); return (
@@ -134,7 +136,7 @@ export default function ProductView({ source, id, onBack }) { - Volver + {t('productView.back')}
@@ -153,37 +155,37 @@ export default function ProductView({ source, id, onBack }) { {isCima ? ( <> {product.active_ingredient && ( - + )} {product.dosage && ( - + )} {product.form && ( - + )} {product.prescription && ( - + )} {product.commercialized !== undefined && ( - + )} ) : ( <> {product.price && ( - + )} {product.original_price && product.original_price > product.price && ( - + )} {product.category && ( - + )} {product.brand && ( - + )} {product.source_url && ( - + )} )} @@ -191,11 +193,11 @@ export default function ProductView({ source, id, onBack }) {
{loadingPharmacies ? ( -
Cargando farmacias...
+
{t('productView.loadingPharmacies')}
) : pharmacies.length > 0 ? ( <>

- Disponible en {pharmacies.length} {pharmacies.length === 1 ? 'farmacia' : 'farmacias'} + {t('productView.availableInPharmacies', { count: pharmacies.length })}

@@ -205,19 +207,19 @@ export default function ProductView({ source, id, onBack }) { disabled={locating} > {locating - ? '📍 Localizando…' + ? t('productView.locating') : sortByDistance - ? '📍 Ordenado por distancia · Reset' - : '📍 Ordenar por distancia'} + ? t('productView.sortedByDistance') + : t('productView.sortByDistance')} {sortByDistance && positionSource && ( - Usando tu ubicación + {t('productView.usingLocation')} )} {locationError && ( {locationError} )}