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, + }; +}