From 69ef048388308c19067a630532e95dc6715056a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoni=20Nu=C3=B1ez=20Romeu?= Date: Tue, 7 Jul 2026 21:04:42 +0200 Subject: [PATCH 1/5] fix: tab bar not visible on Android Add SafeAreaProvider to root layout and use dynamic safe area insets for tab bar height/padding instead of hardcoded values that didn't account for Android system navigation bar. --- apps/frontend-mobile/app/(tabs)/_layout.tsx | 12 ++- apps/frontend-mobile/app/_layout.tsx | 85 +++++++++++---------- 2 files changed, 53 insertions(+), 44 deletions(-) diff --git a/apps/frontend-mobile/app/(tabs)/_layout.tsx b/apps/frontend-mobile/app/(tabs)/_layout.tsx index fc4334c..33f4eca 100644 --- a/apps/frontend-mobile/app/(tabs)/_layout.tsx +++ b/apps/frontend-mobile/app/(tabs)/_layout.tsx @@ -1,6 +1,7 @@ import { Tabs } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; import { View, StyleSheet, Platform } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { colors, shadows } from '../../constants/theme'; function ScanIcon({ color, size }: { color: string; size: number }) { @@ -14,12 +15,19 @@ function ScanIcon({ color, size }: { color: string; size: number }) { } export default function TabLayout() { + const insets = useSafeAreaInsets(); + const bottomPadding = Platform.OS === 'ios' ? insets.bottom : 8; + return ( - - - - - - - - - - - + + + + + + + + + + + + + ); } From aae3ac04af286aabc955398129a9c22cbbe4b47f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoni=20Nu=C3=B1ez=20Romeu?= Date: Tue, 7 Jul 2026 23:39:37 +0200 Subject: [PATCH 2/5] Faro & grafana improvements --- apps/frontend-mobile/.mimocode/.cron-lock | 1 + apps/frontend-mobile/app/(tabs)/_layout.tsx | 32 +++++++++++-- apps/frontend-mobile/app/(tabs)/alerts.tsx | 51 ++++++++++++++++---- apps/frontend-mobile/app/(tabs)/home.tsx | 53 ++++++++++++++++----- apps/frontend-mobile/app/(tabs)/index.tsx | 36 ++++++++++---- apps/frontend-mobile/app/(tabs)/profile.tsx | 16 ++++--- apps/frontend-mobile/app/(tabs)/scan.tsx | 45 +++++++++++++---- apps/frontend-mobile/hooks/useResponsive.ts | 29 +++++++++++ 8 files changed, 213 insertions(+), 50 deletions(-) create mode 100644 apps/frontend-mobile/.mimocode/.cron-lock create mode 100644 apps/frontend-mobile/hooks/useResponsive.ts diff --git a/apps/frontend-mobile/.mimocode/.cron-lock b/apps/frontend-mobile/.mimocode/.cron-lock new file mode 100644 index 0000000..ad22fe7 --- /dev/null +++ b/apps/frontend-mobile/.mimocode/.cron-lock @@ -0,0 +1 @@ +{"pid":1390854,"startedAt":1783459832497} \ No newline at end of file diff --git a/apps/frontend-mobile/app/(tabs)/_layout.tsx b/apps/frontend-mobile/app/(tabs)/_layout.tsx index 33f4eca..6aaf2c6 100644 --- a/apps/frontend-mobile/app/(tabs)/_layout.tsx +++ b/apps/frontend-mobile/app/(tabs)/_layout.tsx @@ -1,9 +1,11 @@ import { Tabs } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; -import { View, StyleSheet, Platform } from 'react-native'; +import { View, StyleSheet, Platform, useWindowDimensions } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { colors, shadows } from '../../constants/theme'; +const TABLET_MIN_WIDTH = 768; + function ScanIcon({ color, size }: { color: string; size: number }) { return ( @@ -16,6 +18,8 @@ function ScanIcon({ color, size }: { color: string; size: number }) { export default function TabLayout() { const insets = useSafeAreaInsets(); + const { width } = useWindowDimensions(); + const isTablet = width >= TABLET_MIN_WIDTH; const bottomPadding = Platform.OS === 'ios' ? insets.bottom : 8; return ( @@ -24,14 +28,20 @@ export default function TabLayout() { tabBarActiveTintColor: colors.primary, tabBarInactiveTintColor: colors.textSecondary, tabBarStyle: { - ...styles.tabBar, - height: 60 + bottomPadding, + ...(isTablet ? styles.tabBarTablet : styles.tabBar), + height: (isTablet ? 64 : 60) + bottomPadding, paddingBottom: bottomPadding, }, - tabBarLabelStyle: styles.tabBarLabel, + tabBarLabelStyle: { + ...styles.tabBarLabel, + ...(isTablet ? styles.tabBarLabelTablet : {}), + }, headerStyle: styles.header, headerTintColor: colors.primary, - headerTitleStyle: styles.headerTitle, + headerTitleStyle: { + ...styles.headerTitle, + ...(isTablet ? styles.headerTitleTablet : {}), + }, }} > = TABLET_MIN_WIDTH; const [items, setItems] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); @@ -127,15 +131,15 @@ export default function AlertsScreen() { return ( - - Notificaciones Guardadas - + + Notificaciones Guardadas + Recibe avisos cuando medicamentos sin stock se repongan {error && ( - + {error} Reintentar @@ -145,9 +149,9 @@ export default function AlertsScreen() { {!error && items.length === 0 && ( - - Sin notificaciones - + + Sin notificaciones + Toca la campana en una farmacia sin stock para recibir notificaciones cuando se reponga. @@ -158,7 +162,7 @@ export default function AlertsScreen() { data={items} keyExtractor={(item) => `${item.scope}:${item.id}`} renderItem={renderItem} - contentContainerStyle={styles.list} + contentContainerStyle={[styles.list, isTablet && styles.listTablet]} showsVerticalScrollIndicator={false} /> )} @@ -176,22 +180,39 @@ const styles = StyleSheet.create({ paddingTop: spacing.lg, paddingBottom: spacing.md, }, + headerTablet: { + maxWidth: 700, + alignSelf: 'center', + width: '100%', + }, title: { fontSize: 22, fontWeight: 'bold', color: colors.text, }, + titleTablet: { + fontSize: 28, + }, subtitle: { fontSize: 14, color: colors.textSecondary, marginTop: spacing.xs, lineHeight: 20, }, + subtitleTablet: { + fontSize: 16, + lineHeight: 24, + }, list: { paddingHorizontal: spacing.lg, paddingBottom: spacing.xl, gap: spacing.sm, }, + listTablet: { + maxWidth: 700, + alignSelf: 'center', + width: '100%', + }, item: { flexDirection: 'row', alignItems: 'center', @@ -249,6 +270,10 @@ const styles = StyleSheet.create({ alignItems: 'center', gap: spacing.sm, }, + errorContainerTablet: { + maxWidth: 700, + alignSelf: 'center', + }, errorText: { fontSize: 14, color: colors.danger, @@ -275,10 +300,18 @@ const styles = StyleSheet.create({ fontWeight: '600', color: colors.text, }, + emptyTitleTablet: { + fontSize: 24, + }, emptyText: { fontSize: 14, color: colors.textSecondary, textAlign: 'center', lineHeight: 20, }, + emptyTextTablet: { + fontSize: 16, + maxWidth: 400, + lineHeight: 24, + }, }); diff --git a/apps/frontend-mobile/app/(tabs)/home.tsx b/apps/frontend-mobile/app/(tabs)/home.tsx index d9f0c97..3a1976d 100644 --- a/apps/frontend-mobile/app/(tabs)/home.tsx +++ b/apps/frontend-mobile/app/(tabs)/home.tsx @@ -1,51 +1,55 @@ import React from 'react'; -import { View, Text, StyleSheet, TouchableOpacity, Image } from 'react-native'; +import { View, Text, StyleSheet, TouchableOpacity, Image, useWindowDimensions } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { useRouter } from 'expo-router'; import { colors, spacing, borderRadius, shadows } from '../../constants/theme'; +const TABLET_MIN_WIDTH = 768; + export default function HomeScreen() { const router = useRouter(); + const { width } = useWindowDimensions(); + const isTablet = width >= TABLET_MIN_WIDTH; return ( - + - FarmaClic - + FarmaClic + Encuentra tus medicamentos en farmacias cercanas - + router.push('/(tabs)')} > - + - Buscar Medicamento + Buscar Medicamento router.push('/scanner')} > - + - Escanear TSI + Escanear TSI @@ -68,17 +72,27 @@ const styles = StyleSheet.create({ gap: spacing.sm, marginBottom: spacing.md, }, + heroTablet: { + marginBottom: spacing.xl, + }, logo: { width: 120, height: 120, marginBottom: spacing.xs, }, + logoTablet: { + width: 160, + height: 160, + }, brandName: { fontSize: 28, fontWeight: 'bold', color: colors.text, letterSpacing: -0.5, }, + brandNameTablet: { + fontSize: 36, + }, description: { fontSize: 16, color: colors.textSecondary, @@ -86,11 +100,19 @@ const styles = StyleSheet.create({ maxWidth: 260, lineHeight: 24, }, + descriptionTablet: { + fontSize: 18, + maxWidth: 400, + lineHeight: 28, + }, cards: { width: '100%', maxWidth: 320, gap: spacing.md, }, + cardsTablet: { + maxWidth: 480, + }, card: { flexDirection: 'row', alignItems: 'center', @@ -101,6 +123,10 @@ const styles = StyleSheet.create({ minHeight: 64, ...shadows.card, }, + cardTablet: { + padding: spacing.lg, + minHeight: 72, + }, cardScan: { backgroundColor: colors.scanButton, }, @@ -131,6 +157,9 @@ const styles = StyleSheet.create({ color: colors.onPrimaryContainer, lineHeight: 24, }, + cardLabelTablet: { + fontSize: 20, + }, cardLabelScan: { color: '#ffffff', }, diff --git a/apps/frontend-mobile/app/(tabs)/index.tsx b/apps/frontend-mobile/app/(tabs)/index.tsx index ebe19f3..d07fec9 100644 --- a/apps/frontend-mobile/app/(tabs)/index.tsx +++ b/apps/frontend-mobile/app/(tabs)/index.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { View, FlatList, StyleSheet, Text, TouchableOpacity } from 'react-native'; +import { View, FlatList, StyleSheet, Text, TouchableOpacity, useWindowDimensions } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { useRouter } from 'expo-router'; import { SearchBar } from '../../components/SearchBar'; @@ -11,13 +11,17 @@ import { colors, spacing, borderRadius } from '../../constants/theme'; import { Medicine } from '../../types'; import { config } from '../../constants/config'; +const TABLET_MIN_WIDTH = 768; + export default function HomeScreen() { const router = useRouter(); + const { width } = useWindowDimensions(); + const isTablet = width >= TABLET_MIN_WIDTH; const [query, setQuery] = useState(''); const [results, setResults] = useState([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); - + const debouncedQuery = useDebounce(query, config.SEARCH_DEBOUNCE_MS); useEffect(() => { @@ -49,7 +53,7 @@ export default function HomeScreen() { return ( - + - + {isLoading && } - + {error && ( - + {error} )} - + {!isLoading && !error && results.length === 0 && query.length >= 2 && ( No se encontraron medicamentos )} - + item.nregistro} renderItem={({ item }) => } - contentContainerStyle={styles.list} + contentContainerStyle={[styles.list, isTablet && styles.listTablet]} showsVerticalScrollIndicator={false} /> @@ -98,6 +102,11 @@ const styles = StyleSheet.create({ alignItems: 'center', paddingRight: spacing.lg, }, + searchContainerTablet: { + maxWidth: 700, + alignSelf: 'center', + width: '100%', + }, scannerButton: { width: 44, height: 44, @@ -114,12 +123,21 @@ const styles = StyleSheet.create({ list: { paddingBottom: spacing.xl, }, + listTablet: { + maxWidth: 700, + alignSelf: 'center', + width: '100%', + }, errorContainer: { padding: spacing.md, marginHorizontal: spacing.lg, backgroundColor: colors.dangerContainer, borderRadius: borderRadius.lg, }, + errorContainerTablet: { + maxWidth: 700, + alignSelf: 'center', + }, errorText: { color: colors.danger, textAlign: 'center', diff --git a/apps/frontend-mobile/app/(tabs)/profile.tsx b/apps/frontend-mobile/app/(tabs)/profile.tsx index 5d6c3c6..17ab6cf 100644 --- a/apps/frontend-mobile/app/(tabs)/profile.tsx +++ b/apps/frontend-mobile/app/(tabs)/profile.tsx @@ -1,11 +1,13 @@ import React, { useState, useEffect } from 'react'; -import { View, Text, StyleSheet, TouchableOpacity, Alert, TextInput, ScrollView, Image, ActivityIndicator, Modal, Pressable } from 'react-native'; +import { View, Text, StyleSheet, TouchableOpacity, Alert, TextInput, ScrollView, Image, ActivityIndicator, Modal, Pressable, useWindowDimensions } from 'react-native'; import { useRouter } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; import * as ImagePicker from 'expo-image-picker'; import { useAuth } from '../../hooks/useAuth'; import { colors, spacing, borderRadius } from '../../constants/theme'; +const TABLET_MIN_WIDTH = 768; + const AVATARS = [ require('../../assets/avatars/avatar1.png'), require('../../assets/avatars/avatar2.png'), @@ -44,6 +46,8 @@ interface Address { export default function ProfileScreen() { const router = useRouter(); + const { width } = useWindowDimensions(); + const isTablet = width >= TABLET_MIN_WIDTH; const { user, isAuthenticated, isLoading, logout, isAdmin } = useAuth(); const [firstName, setFirstName] = useState(user?.first_name || ''); @@ -388,16 +392,16 @@ export default function ProfileScreen() { return ( - - Inicia Sesión - + + Inicia Sesión + Inicia sesión para acceder a todas las funcionalidades router.push('/auth/login')} > - Iniciar Sesión + Iniciar Sesión = TABLET_MIN_WIDTH; return ( - - - + + + - Escanear TSI - + Escanear TSI + Escanea el código de barras de tu tarjeta sanitaria para encontrar tus medicamentos router.push('/scanner')} > - - Iniciar escaneo + + Iniciar escaneo @@ -42,6 +46,9 @@ const styles = StyleSheet.create({ paddingHorizontal: spacing.xl, gap: spacing.md, }, + contentTablet: { + gap: spacing.lg, + }, iconContainer: { width: 100, height: 100, @@ -51,11 +58,19 @@ const styles = StyleSheet.create({ justifyContent: 'center', marginBottom: spacing.sm, }, + iconContainerTablet: { + width: 140, + height: 140, + borderRadius: 70, + }, title: { fontSize: 22, fontWeight: 'bold', color: colors.text, }, + titleTablet: { + fontSize: 28, + }, description: { fontSize: 15, color: colors.textSecondary, @@ -63,6 +78,11 @@ const styles = StyleSheet.create({ lineHeight: 22, maxWidth: 280, }, + descriptionTablet: { + fontSize: 17, + maxWidth: 420, + lineHeight: 26, + }, button: { flexDirection: 'row', alignItems: 'center', @@ -73,9 +93,16 @@ const styles = StyleSheet.create({ paddingHorizontal: spacing.xl, marginTop: spacing.md, }, + buttonTablet: { + paddingVertical: spacing.lg, + paddingHorizontal: spacing.xl * 1.5, + }, buttonText: { fontSize: 17, fontWeight: '600', color: '#ffffff', }, + buttonTextTablet: { + fontSize: 20, + }, }); diff --git a/apps/frontend-mobile/hooks/useResponsive.ts b/apps/frontend-mobile/hooks/useResponsive.ts new file mode 100644 index 0000000..60f0261 --- /dev/null +++ b/apps/frontend-mobile/hooks/useResponsive.ts @@ -0,0 +1,29 @@ +import { useWindowDimensions } from 'react-native'; + +const TABLET_MIN_WIDTH = 768; + +export function useResponsive() { + const { width, height } = useWindowDimensions(); + + const isTablet = width >= TABLET_MIN_WIDTH; + const isLandscape = width > height; + + // Scale factor for tablet: 1.0 on phone, ~1.2 on tablet + const scale = isTablet ? 1.2 : 1.0; + + // Max content width to prevent stretching on large screens + const maxContentWidth = isTablet ? Math.min(600, width * 0.6) : width; + + // Horizontal padding: more on tablets to center content + const horizontalPadding = isTablet ? 48 : 24; + + return { + width, + height, + isTablet, + isLandscape, + scale, + maxContentWidth, + horizontalPadding, + }; +} From 6612c8b10cb250b6ef2e762519258181e31fffa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoni=20Nu=C3=B1ez=20Romeu?= Date: Tue, 7 Jul 2026 23:40:04 +0200 Subject: [PATCH 3/5] Faro & grafana improvements --- apps/frontend-mobile/app/(tabs)/profile.tsx | 16 +++--- apps/frontend/src/App.jsx | 16 +++++- .../frontend/src/components/ErrorBoundary.jsx | 57 +++++++++++++++++++ apps/frontend/src/main.jsx | 13 ++++- apps/frontend/src/utils/faro.js | 10 +++- 5 files changed, 99 insertions(+), 13 deletions(-) create mode 100644 apps/frontend/src/components/ErrorBoundary.jsx diff --git a/apps/frontend-mobile/app/(tabs)/profile.tsx b/apps/frontend-mobile/app/(tabs)/profile.tsx index 17ab6cf..8fac40d 100644 --- a/apps/frontend-mobile/app/(tabs)/profile.tsx +++ b/apps/frontend-mobile/app/(tabs)/profile.tsx @@ -417,12 +417,12 @@ export default function ProfileScreen() { return ( {/* Avatar Section */} - - setShowAvatarModal(true)}> + + setShowAvatarModal(true)}> {avatarUrl ? ( - + ) : ( - + )} @@ -432,7 +432,7 @@ export default function ProfileScreen() { {/* Read-only Name Section */} - + Nombre {firstName || '—'} @@ -444,7 +444,7 @@ export default function ProfileScreen() { {/* Menu Section */} - + Configuración @@ -463,7 +463,7 @@ export default function ProfileScreen() { {searchHistory.map((item) => ( {item.address} - handleDeleteSearch(item.id)} style={styles.searchDelete} > @@ -484,7 +484,7 @@ export default function ProfileScreen() { {/* Logout Button */} - + Cerrar Sesión diff --git a/apps/frontend/src/App.jsx b/apps/frontend/src/App.jsx index 5b4d14e..768b5b6 100644 --- a/apps/frontend/src/App.jsx +++ b/apps/frontend/src/App.jsx @@ -9,6 +9,7 @@ import AdminView from './views/AdminView'; import LoginModal from './components/LoginModal'; import SavedNotifications from './components/SavedNotifications'; import BottomNav from './components/BottomNav'; +import { getFaro } from './utils/faro'; function App() { const [screen, setScreen] = useState('home'); @@ -40,7 +41,10 @@ function App() { fetch('/api/auth/check') .then(r => r.json()) .then(data => { if (data.authenticated) setCurrentUser(data.user); }) - .catch(() => {}) + .catch((err) => { + console.warn('[auth/check]', err); + getFaro()?.pushError(err, { type: 'network', url: '/api/auth/check' }); + }) .finally(() => setAuthChecked(true)); window.addEventListener('resize', handleResize); @@ -57,7 +61,10 @@ function App() { const count = (data.global?.length || 0) + (data.pharmacy?.length || 0); setBadgeCount(count); }) - .catch(() => {}); + .catch((err) => { + console.warn('[notifications/mine]', err); + getFaro()?.pushError(err, { type: 'network', url: '/api/notifications/mine' }); + }); return () => { cancelled = true; }; }, [currentUser]); @@ -69,7 +76,10 @@ function App() { if (!data) return; setBadgeCount((data.global?.length || 0) + (data.pharmacy?.length || 0)); }) - .catch(() => {}); + .catch((err) => { + console.warn('[refreshBadge]', err); + getFaro()?.pushError(err, { type: 'network', url: '/api/notifications/mine' }); + }); } function handleLogin(user) { diff --git a/apps/frontend/src/components/ErrorBoundary.jsx b/apps/frontend/src/components/ErrorBoundary.jsx new file mode 100644 index 0000000..97044b6 --- /dev/null +++ b/apps/frontend/src/components/ErrorBoundary.jsx @@ -0,0 +1,57 @@ +import React from 'react'; + +export default class ErrorBoundary extends React.Component { + constructor(props) { + super(props); + this.state = { hasError: false, error: null }; + } + + static getDerivedStateFromError(error) { + return { hasError: true, error }; + } + + componentDidCatch(error, info) { + // Report to Faro if available + try { + const faro = window.__faro; + if (faro?.api?.pushError) { + faro.api.pushError(error, { type: 'react-boundary', componentStack: info.componentStack }); + } + } catch { + // Faro reporting must never break the app + } + console.error('[ErrorBoundary]', error, info); + } + + render() { + if (this.state.hasError) { + return ( +
+

Algo salió mal

+

Ha ocurrido un error inesperado. Por favor, recarga la página.

+ +
+ ); + } + return this.props.children; + } +} diff --git a/apps/frontend/src/main.jsx b/apps/frontend/src/main.jsx index 4ed469c..e0341e3 100644 --- a/apps/frontend/src/main.jsx +++ b/apps/frontend/src/main.jsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; +import ErrorBoundary from './components/ErrorBoundary'; import './index.css'; import { initNativeShell } from './utils/native'; import { initFaro } from './utils/faro'; @@ -9,9 +10,19 @@ import { initFaro } from './utils/faro'; // No-op if VITE_FARO_ENDPOINT is not configured. initFaro(); +// Global unhandled promise rejection handler — report to Faro +window.addEventListener('unhandledrejection', (event) => { + console.warn('[unhandledrejection]', event.reason); + try { + window.__faro?.api?.pushError(event.reason, { type: 'unhandled-rejection' }); + } catch { /* Faro must never break the app */ } +}); + ReactDOM.createRoot(document.getElementById('root')).render( - + + + ); diff --git a/apps/frontend/src/utils/faro.js b/apps/frontend/src/utils/faro.js index 50bc81e..43c5ed4 100644 --- a/apps/frontend/src/utils/faro.js +++ b/apps/frontend/src/utils/faro.js @@ -17,6 +17,11 @@ import { TracingInstrumentation } from '@grafana/faro-web-tracing'; import { OtlpHttpTransport } from '@grafana/faro-transport-otlp-http'; let initialized = false; +let faroApi = null; + +export function getFaro() { + return faroApi; +} export function initFaro() { if (initialized) return; @@ -34,7 +39,7 @@ export function initFaro() { const version = import.meta.env.VITE_FARO_APP_VERSION || '1.0.0'; try { - initializeFaro({ + const faro = initializeFaro({ url: `${endpoint.replace(/\/$/, '')}/v1/traces`, app: { name: appName, @@ -61,6 +66,9 @@ export function initFaro() { url: `${endpoint.replace(/\/$/, '')}/v1/traces`, }), }); + faroApi = faro.api; + // Expose for ErrorBoundary and manual reporting + window.__faro = faro; } catch (err) { // Never let Faro init failure break the app. // eslint-disable-next-line no-console From fe08dd512e284ef43253ea55cec2bf35fad7c550 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoni=20Nu=C3=B1ez=20Romeu?= Date: Tue, 7 Jul 2026 23:40:40 +0200 Subject: [PATCH 4/5] Faro & grafana improvements 2 --- apps/frontend-mobile/app/(tabs)/profile.tsx | 42 +++++++++++++++++++ .../components/MedicineCard.tsx | 40 ++++++++++++++---- 2 files changed, 74 insertions(+), 8 deletions(-) diff --git a/apps/frontend-mobile/app/(tabs)/profile.tsx b/apps/frontend-mobile/app/(tabs)/profile.tsx index 8fac40d..a2411d0 100644 --- a/apps/frontend-mobile/app/(tabs)/profile.tsx +++ b/apps/frontend-mobile/app/(tabs)/profile.tsx @@ -818,6 +818,9 @@ const styles = StyleSheet.create({ color: colors.text, marginTop: spacing.lg, }, + authTitleTablet: { + fontSize: 32, + }, authSubtitle: { fontSize: 16, color: colors.textSecondary, @@ -825,11 +828,18 @@ const styles = StyleSheet.create({ marginTop: spacing.sm, marginBottom: spacing.xl, }, + authSubtitleTablet: { + fontSize: 18, + maxWidth: 400, + }, avatarSection: { alignItems: 'center', padding: spacing.xl, backgroundColor: colors.card, }, + avatarSectionTablet: { + paddingVertical: spacing.xxl, + }, avatarContainer: { width: 100, height: 100, @@ -839,11 +849,21 @@ const styles = StyleSheet.create({ alignItems: 'center', overflow: 'hidden', }, + avatarContainerTablet: { + width: 140, + height: 140, + borderRadius: 70, + }, avatarImage: { width: 100, height: 100, borderRadius: 50, }, + avatarImageTablet: { + width: 140, + height: 140, + borderRadius: 70, + }, avatarOverlay: { position: 'absolute', top: 0, @@ -866,6 +886,11 @@ const styles = StyleSheet.create({ marginTop: spacing.sm, gap: spacing.md, }, + infoSectionTablet: { + maxWidth: 600, + alignSelf: 'center', + width: '100%', + }, infoCard: { backgroundColor: colors.surfaceLow, padding: spacing.md, @@ -890,6 +915,11 @@ const styles = StyleSheet.create({ marginTop: spacing.sm, backgroundColor: colors.card, }, + menuSectionTablet: { + maxWidth: 600, + alignSelf: 'center', + width: '100%', + }, menuItem: { flexDirection: 'row', alignItems: 'center', @@ -936,11 +966,18 @@ const styles = StyleSheet.create({ paddingVertical: spacing.md, paddingHorizontal: spacing.xl, }, + buttonTablet: { + paddingVertical: spacing.lg, + paddingHorizontal: spacing.xl * 1.5, + }, buttonText: { color: colors.textInverse, fontSize: 16, fontWeight: '600', }, + buttonTextTablet: { + fontSize: 18, + }, linkButton: { marginTop: spacing.md, }, @@ -959,6 +996,11 @@ const styles = StyleSheet.create({ borderWidth: 1, borderColor: colors.danger, }, + logoutButtonTablet: { + maxWidth: 600, + alignSelf: 'center', + width: '100%', + }, logoutText: { marginLeft: spacing.sm, color: colors.danger, diff --git a/apps/frontend-mobile/components/MedicineCard.tsx b/apps/frontend-mobile/components/MedicineCard.tsx index 6799c7c..e7649e1 100644 --- a/apps/frontend-mobile/components/MedicineCard.tsx +++ b/apps/frontend-mobile/components/MedicineCard.tsx @@ -1,46 +1,54 @@ import React from 'react'; -import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; +import { View, Text, StyleSheet, TouchableOpacity, useWindowDimensions } 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'; +const TABLET_MIN_WIDTH = 768; + interface MedicineCardProps { medicine: Medicine; } export function MedicineCard({ medicine }: MedicineCardProps) { const router = useRouter(); + const { width } = useWindowDimensions(); + const isTablet = width >= TABLET_MIN_WIDTH; const handlePress = () => { router.push(`/medicine/${medicine.nregistro}`); }; return ( - + - + {medicine.nombre} - + {medicine.principioActivo} - + - + {medicine.precio ? `${medicine.precio.toFixed(2)} €` : 'Sin precio'} - + - + {medicine.laboratorio} @@ -62,6 +70,12 @@ const styles = StyleSheet.create({ shadowRadius: 8, elevation: 2, }, + cardTablet: { + padding: spacing.lg, + maxWidth: 700, + alignSelf: 'center', + width: '100%', + }, header: { flexDirection: 'row', justifyContent: 'space-between', @@ -75,6 +89,9 @@ const styles = StyleSheet.create({ color: colors.text, marginRight: spacing.sm, }, + nameTablet: { + fontSize: 18, + }, principioActivo: { fontSize: 14, color: colors.textSecondary, @@ -95,6 +112,9 @@ const styles = StyleSheet.create({ color: colors.primary, marginLeft: spacing.xs, }, + priceTablet: { + fontSize: 16, + }, labContainer: { flexDirection: 'row', alignItems: 'center', @@ -107,4 +127,8 @@ const styles = StyleSheet.create({ marginLeft: spacing.xs, maxWidth: 120, }, + laboratorioTablet: { + fontSize: 14, + maxWidth: 200, + }, }); From 0099aab09342eb7523542c5fa888b20873f4f80e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoni=20Nu=C3=B1ez=20Romeu?= Date: Tue, 7 Jul 2026 23:43:18 +0200 Subject: [PATCH 5/5] mobile ui fix --- apps/frontend-mobile/components/SearchBar.tsx | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/apps/frontend-mobile/components/SearchBar.tsx b/apps/frontend-mobile/components/SearchBar.tsx index 73553d1..02038ed 100644 --- a/apps/frontend-mobile/components/SearchBar.tsx +++ b/apps/frontend-mobile/components/SearchBar.tsx @@ -1,8 +1,10 @@ import React, { useState } from 'react'; -import { View, TextInput, StyleSheet, TouchableOpacity } from 'react-native'; +import { View, TextInput, StyleSheet, TouchableOpacity, useWindowDimensions } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { colors, spacing, borderRadius } from '../constants/theme'; +const TABLET_MIN_WIDTH = 768; + interface SearchBarProps { placeholder?: string; onSearch: (query: string) => void; @@ -10,12 +12,14 @@ interface SearchBarProps { onChangeText?: (text: string) => void; } -export function SearchBar({ - placeholder = 'Buscar medicamentos...', +export function SearchBar({ + placeholder = 'Buscar medicamentos...', onSearch, value, - onChangeText + onChangeText }: SearchBarProps) { + const { width } = useWindowDimensions(); + const isTablet = width >= TABLET_MIN_WIDTH; const [localValue, setLocalValue] = useState(value || ''); const handleChange = (text: string) => { @@ -34,10 +38,10 @@ export function SearchBar({ }; return ( - +